Separate endpoint concerns

Update endpoint code to provide cleaner separation of concerns.
Specifically, the top level endpoint package is no longer aware of
the fact that JMX and HTTP are ultimately used to expose endpoints.
Caching concerns have also been abstracted behind a general purpose
`OperationMethodInvokerAdvisor` interface.

Configuration properties have been refined to further enforce
separation. The `management.endpoint.<name>` prefix provides
configuration for a  single endpoint (including enable and cache
time-to-live). These  properties are now technology agnostic (they
don't include `web` or `jmx` sub properties).

The `management.endpoints.<technology>` prefix provide exposure specific
configuration. For example, `management.endpoints.web.path-mapping`
allow endpoint URLs to be changed.

Endpoint enabled/disabled logic has been simplified so that endpoints
can't be disabled per exposure technology. Instead a filter based
approach is used to allow refinement of what endpoints are exposed over
a given technology.

Fixes gh-10176
This commit is contained in:
Phillip Webb
2017-10-13 09:14:27 -07:00
parent d24709c696
commit fd5c43cdc9
169 changed files with 3424 additions and 3067 deletions

View File

@@ -23,10 +23,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -42,6 +42,7 @@ import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
@@ -157,9 +158,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
}
TypeElement endpointType = elementUtils.getTypeElement(endpointAnnotation());
if (endpointType != null) { // Is @Endpoint available
for (Element element : roundEnv.getElementsAnnotatedWith(endpointType)) {
processEndpoint(element);
}
getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType)
.forEach(this::processEndpoint);
}
if (roundEnv.processingOver()) {
try {
@@ -172,6 +172,41 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return false;
}
private Map<Element, List<Element>> getElementsAnnotatedOrMetaAnnotatedWith(
RoundEnvironment roundEnv, TypeElement annotation) {
DeclaredType annotationType = (DeclaredType) annotation.asType();
Map<Element, List<Element>> result = new LinkedHashMap<>();
for (Element element : roundEnv.getRootElements()) {
LinkedList<Element> stack = new LinkedList<>();
stack.push(element);
collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack);
stack.removeFirst();
if (!stack.isEmpty()) {
result.put(element, Collections.unmodifiableList(stack));
}
}
return result;
}
private boolean collectElementsAnnotatedOrMetaAnnotatedWith(
DeclaredType annotationType, LinkedList<Element> stack) {
Element element = stack.peekLast();
for (AnnotationMirror annotation : this.processingEnv.getElementUtils()
.getAllAnnotationMirrors(element)) {
Element annotationElement = annotation.getAnnotationType().asElement();
if (!stack.contains(annotationElement)) {
stack.addLast(annotationElement);
if (annotationElement.equals(annotationType.asElement())) {
return true;
}
if (!collectElementsAnnotatedOrMetaAnnotatedWith(annotationType, stack)) {
stack.removeLast();
}
}
}
return false;
}
private void processElement(Element element) {
try {
AnnotationMirror annotation = getAnnotation(element,
@@ -360,9 +395,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
}
}
private void processEndpoint(Element element) {
private void processEndpoint(Element element, List<Element> annotations) {
try {
AnnotationMirror annotation = getAnnotation(element, endpointAnnotation());
String annotationName = this.typeUtils.getQualifiedName(annotations.get(0));
AnnotationMirror annotation = getAnnotation(element, annotationName);
if (element instanceof TypeElement) {
processEndpoint(annotation, (TypeElement) element);
}
@@ -379,51 +415,17 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
if (endpointId == null || "".equals(endpointId)) {
return; // Can't process that endpoint
}
Boolean enabledByDefault = determineEnabledByDefault(
elementValues.get("defaultEnablement"));
Boolean enabledByDefault = (Boolean) elementValues.get("enableByDefault");
String type = this.typeUtils.getQualifiedName(element);
this.metadataCollector
.add(ItemMetadata.newGroup(endpointKey(endpointId), type, type, null));
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey(endpointId),
"enabled", Boolean.class.getName(), type, null,
String.format("Enable the %s endpoint.", endpointId), enabledByDefault,
null));
String.format("Enable the %s endpoint.", endpointId),
(enabledByDefault == null ? true : enabledByDefault), null));
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey(endpointId),
"cache.time-to-live", Long.class.getName(), type, null,
"Maximum time in milliseconds that a response can be cached.", 0, null));
EndpointExposure endpointTypes = EndpointExposure
.parse(elementValues.get("exposure"));
if (endpointTypes.hasJmx()) {
this.metadataCollector.add(ItemMetadata.newProperty(
endpointKey(endpointId + ".jmx"), "enabled", Boolean.class.getName(),
type, null,
String.format("Expose the %s endpoint as a JMX MBean.", endpointId),
enabledByDefault, null));
}
if (endpointTypes.hasWeb()) {
this.metadataCollector.add(ItemMetadata.newProperty(
endpointKey(endpointId + ".web"), "enabled", Boolean.class.getName(),
type, null, String.format("Expose the %s endpoint as a Web endpoint.",
endpointId),
enabledByDefault, null));
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey(endpointId),
"web.path", String.class.getName(), type, null,
String.format("Path of the %s endpoint.", endpointId), endpointId,
null));
}
}
private Boolean determineEnabledByDefault(Object defaultEnablement) {
if (defaultEnablement != null) {
String value = String.valueOf(defaultEnablement);
if ("ENABLED".equals(value)) {
return true;
}
if ("DISABLED".equals(value)) {
return false;
}
}
return null;
}
private String endpointKey(String suffix) {
@@ -550,46 +552,4 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
this.processingEnv.getMessager().printMessage(kind, msg);
}
private static class EndpointExposure {
private static final List<String> ALL = Arrays.asList("JMX", "WEB");
private final List<String> types;
EndpointExposure(List<String> types) {
this.types = types;
}
static EndpointExposure parse(Object exposureAttribute) {
List<AnnotationValue> values = asAnnotationValues(exposureAttribute);
if (values.isEmpty()) {
return new EndpointExposure(ALL);
}
return new EndpointExposure(
values.stream().map(EndpointExposure::getValueAttribute)
.collect(Collectors.toList()));
}
@SuppressWarnings("unchecked")
private static List<AnnotationValue> asAnnotationValues(Object typesAttribute) {
if (!(typesAttribute instanceof List)) {
return Collections.emptyList();
}
return (List<AnnotationValue>) typesAttribute;
}
private static String getValueAttribute(AnnotationValue value) {
return ((VariableElement) value.getValue()).getSimpleName().toString();
}
public boolean hasJmx() {
return this.types.contains("JMX");
}
public boolean hasWeb() {
return this.types.contains("WEB");
}
}
}

View File

@@ -39,11 +39,9 @@ import org.springframework.boot.configurationprocessor.metadata.TestJsonConverte
import org.springframework.boot.configurationsample.endpoint.CustomPropertiesEndpoint;
import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint;
import org.springframework.boot.configurationsample.endpoint.EnabledEndpoint;
import org.springframework.boot.configurationsample.endpoint.OnlyJmxEndpoint;
import org.springframework.boot.configurationsample.endpoint.OnlyWebEndpoint;
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint;
import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint;
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint;
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalJmxEndpoint;
import org.springframework.boot.configurationsample.incremental.BarProperties;
import org.springframework.boot.configurationsample.incremental.FooProperties;
import org.springframework.boot.configurationsample.incremental.RenamedBarProperties;
@@ -536,12 +534,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
ConfigurationMetadata metadata = compile(SimpleEndpoint.class);
assertThat(metadata).has(
Metadata.withGroup("endpoints.simple").fromSource(SimpleEndpoint.class));
assertThat(metadata).has(enabledFlag("simple", null));
assertThat(metadata).has(jmxEnabledFlag("simple", null));
assertThat(metadata).has(webEnabledFlag("simple", null));
assertThat(metadata).has(webPath("simple"));
assertThat(metadata).has(enabledFlag("simple", true));
assertThat(metadata).has(cacheTtl("simple"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(3);
}
@Test
@@ -550,11 +545,8 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(Metadata.withGroup("endpoints.disabled")
.fromSource(DisabledEndpoint.class));
assertThat(metadata).has(enabledFlag("disabled", false));
assertThat(metadata).has(jmxEnabledFlag("disabled", false));
assertThat(metadata).has(webEnabledFlag("disabled", false));
assertThat(metadata).has(webPath("disabled"));
assertThat(metadata).has(cacheTtl("disabled"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(3);
}
@Test
@@ -563,11 +555,8 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(Metadata.withGroup("endpoints.enabled")
.fromSource(EnabledEndpoint.class));
assertThat(metadata).has(enabledFlag("enabled", true));
assertThat(metadata).has(jmxEnabledFlag("enabled", true));
assertThat(metadata).has(webEnabledFlag("enabled", true));
assertThat(metadata).has(webPath("enabled"));
assertThat(metadata).has(cacheTtl("enabled"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(3);
}
@Test
@@ -577,35 +566,19 @@ public class ConfigurationMetadataAnnotationProcessorTests {
.fromSource(CustomPropertiesEndpoint.class));
assertThat(metadata).has(Metadata.withProperty("endpoints.customprops.name")
.ofType(String.class).withDefaultValue("test"));
assertThat(metadata).has(enabledFlag("customprops", null));
assertThat(metadata).has(jmxEnabledFlag("customprops", null));
assertThat(metadata).has(webEnabledFlag("customprops", null));
assertThat(metadata).has(webPath("customprops"));
assertThat(metadata).has(enabledFlag("customprops", true));
assertThat(metadata).has(cacheTtl("customprops"));
assertThat(metadata.getItems()).hasSize(7);
}
@Test
public void jmxOnlyEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(OnlyJmxEndpoint.class);
assertThat(metadata).has(
Metadata.withGroup("endpoints.jmx").fromSource(OnlyJmxEndpoint.class));
assertThat(metadata).has(enabledFlag("jmx", null));
assertThat(metadata).has(jmxEnabledFlag("jmx", null));
assertThat(metadata).has(cacheTtl("jmx"));
assertThat(metadata.getItems()).hasSize(4);
}
@Test
public void webOnlyEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(OnlyWebEndpoint.class);
assertThat(metadata).has(
Metadata.withGroup("endpoints.web").fromSource(OnlyWebEndpoint.class));
assertThat(metadata).has(enabledFlag("web", null));
assertThat(metadata).has(webEnabledFlag("web", null));
assertThat(metadata).has(webPath("web"));
assertThat(metadata).has(cacheTtl("web"));
assertThat(metadata.getItems()).hasSize(5);
public void specificEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(SpecificEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.specific")
.fromSource(SpecificEndpoint.class));
assertThat(metadata).has(enabledFlag("specific", true));
assertThat(metadata).has(cacheTtl("specific"));
assertThat(metadata.getItems()).hasSize(3);
}
@Test
@@ -615,74 +588,37 @@ public class ConfigurationMetadataAnnotationProcessorTests {
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(enabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(3);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", defaultEnablement = org.springframework.boot."
+ "configurationsample.DefaultEnablement.DISABLED");
"id = \"incremental\", enableByDefault = false");
metadata = project.incrementalBuild(IncrementalEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", false));
assertThat(metadata).has(jmxEnabledFlag("incremental", false));
assertThat(metadata).has(webEnabledFlag("incremental", false));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(3);
}
@Test
public void incrementalEndpointBuildDisableJmxEndpoint() throws Exception {
public void incrementalEndpointBuildEnableSpecificEndpoint() throws Exception {
TestProject project = new TestProject(this.temporaryFolder,
IncrementalEndpoint.class);
SpecificEndpoint.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(6);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", exposure = org.springframework.boot."
+ "configurationsample.EndpointExposure.WEB");
metadata = project.incrementalBuild(IncrementalEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
}
@Test
public void incrementalEndpointBuildEnableJmxEndpoint() throws Exception {
TestProject project = new TestProject(this.temporaryFolder,
IncrementalJmxEndpoint.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalJmxEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(4);
project.replaceText(IncrementalJmxEndpoint.class,
", exposure = EndpointExposure.JMX", "");
metadata = project.incrementalBuild(IncrementalJmxEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalJmxEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata).has(Metadata.withGroup("endpoints.specific")
.fromSource(SpecificEndpoint.class));
assertThat(metadata).has(enabledFlag("specific", true));
assertThat(metadata).has(cacheTtl("specific"));
assertThat(metadata.getItems()).hasSize(3);
project.replaceText(SpecificEndpoint.class, "enableByDefault = true",
"enableByDefault = false");
metadata = project.incrementalBuild(SpecificEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.specific")
.fromSource(SpecificEndpoint.class));
assertThat(metadata).has(enabledFlag("specific", false));
assertThat(metadata).has(cacheTtl("specific"));
assertThat(metadata.getItems()).hasSize(3);
}
private Metadata.MetadataItemCondition enabledFlag(String endpointId,
@@ -692,26 +628,6 @@ public class ConfigurationMetadataAnnotationProcessorTests {
.withDescription(String.format("Enable the %s endpoint.", endpointId));
}
private Metadata.MetadataItemCondition jmxEnabledFlag(String endpointId,
Boolean defaultValue) {
return Metadata.withEnabledFlag("endpoints." + endpointId + ".jmx.enabled")
.withDefaultValue(defaultValue).withDescription(String
.format("Expose the %s endpoint as a JMX MBean.", endpointId));
}
private Metadata.MetadataItemCondition webEnabledFlag(String endpointId,
Boolean defaultValue) {
return Metadata.withEnabledFlag("endpoints." + endpointId + ".web.enabled")
.withDefaultValue(defaultValue).withDescription(String
.format("Expose the %s endpoint as a Web endpoint.", endpointId));
}
private Metadata.MetadataItemCondition webPath(String endpointId) {
return Metadata.withProperty("endpoints." + endpointId + ".web.path")
.ofType(String.class).withDefaultValue(endpointId)
.withDescription(String.format("Path of the %s endpoint.", endpointId));
}
private Metadata.MetadataItemCondition cacheTtl(String endpointId) {
return Metadata.withProperty("endpoints." + endpointId + ".cache.time-to-live")
.ofType(Long.class).withDefaultValue(0).withDescription(

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample;
public enum DefaultEnablement {
ENABLED, DISABLED, NEUTRAL
}

View File

@@ -33,10 +33,8 @@ import java.lang.annotation.Target;
@Documented
public @interface Endpoint {
String id();
String id() default "";
DefaultEnablement defaultEnablement() default DefaultEnablement.NEUTRAL;
EndpointExposure[] exposure() default {};
boolean enableByDefault() default true;
}

View File

@@ -16,10 +16,20 @@
package org.springframework.boot.configurationsample;
public enum EndpointExposure {
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
JMX,
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Endpoint
public @interface MetaEndpoint {
WEB
String id();
boolean enableByDefault() default true;
}

View File

@@ -16,7 +16,6 @@
package org.springframework.boot.configurationsample.endpoint;
import org.springframework.boot.configurationsample.DefaultEnablement;
import org.springframework.boot.configurationsample.Endpoint;
/**
@@ -24,7 +23,7 @@ import org.springframework.boot.configurationsample.Endpoint;
*
* @author Stephane Nicoll
*/
@Endpoint(id = "disabled", defaultEnablement = DefaultEnablement.DISABLED)
@Endpoint(id = "disabled", enableByDefault = false)
public class DisabledEndpoint {
}

View File

@@ -16,7 +16,6 @@
package org.springframework.boot.configurationsample.endpoint;
import org.springframework.boot.configurationsample.DefaultEnablement;
import org.springframework.boot.configurationsample.Endpoint;
/**
@@ -24,7 +23,7 @@ import org.springframework.boot.configurationsample.Endpoint;
*
* @author Stephane Nicoll
*/
@Endpoint(id = "enabled", defaultEnablement = DefaultEnablement.ENABLED)
@Endpoint(id = "enabled")
public class EnabledEndpoint {
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.endpoint;
import org.springframework.boot.configurationsample.Endpoint;
import org.springframework.boot.configurationsample.EndpointExposure;
/**
* An endpoints that only exposes a web endpoint.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "web", exposure = EndpointExposure.WEB)
public class OnlyWebEndpoint {
}

View File

@@ -16,15 +16,15 @@
package org.springframework.boot.configurationsample.endpoint;
import org.springframework.boot.configurationsample.Endpoint;
import org.springframework.boot.configurationsample.EndpointExposure;
import org.springframework.boot.configurationsample.MetaEndpoint;
/**
* An endpoint that only exposes a JMX MBean.
* An meta-annotated endpoint similar to {@code @WebEndpoint} or {@code @JmxEndpoint} in
* Boot.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "jmx", exposure = EndpointExposure.JMX)
public class OnlyJmxEndpoint {
@MetaEndpoint(id = "specific", enableByDefault = true)
public class SpecificEndpoint {
}

View File

@@ -16,15 +16,15 @@
package org.springframework.boot.configurationsample.endpoint.incremental;
import org.springframework.boot.configurationsample.Endpoint;
import org.springframework.boot.configurationsample.EndpointExposure;
import org.springframework.boot.configurationsample.MetaEndpoint;
/**
* An endpoint that only exposes a JMX MBean.
* An meta-annotated endpoint similar to {@code @WebEndpoint} or {@code @JmxEndpoint} in
* Boot.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "incremental", exposure = EndpointExposure.JMX)
public class IncrementalJmxEndpoint {
@MetaEndpoint(id = "incremental")
public class IncrementalSpecificEndpoint {
}