Add support of metadata generation for Endpoints

This commit improves the configuration metadata annotation processor to
explicitly handle `@Endpoint` annotated class. Adding a new endpoint on
a project potentially creates the following keys:

* `endpoints.<id>.enabled`
* `endpoints.<id>.cache.time-to-live`
* `endpoints.<id>.jmx.enabled`
* `endpoints.<id>.web.enabled`

Default values are extracted from the annotation type. If an endpoint
is restricted to a given tech, properties from unrelated techs are not
generated.

Closes gh-9692
This commit is contained in:
Stephane Nicoll
2017-07-07 15:51:44 +01:00
parent 25d2d55383
commit 83e81e13f2
13 changed files with 558 additions and 0 deletions

View File

@@ -36,6 +36,13 @@ import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import org.springframework.boot.configurationprocessor.metadata.TestJsonConverter;
import org.springframework.boot.configurationsample.endpoint.CustomPropertiesEndpoint;
import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint;
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.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;
@@ -517,6 +524,164 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata.getItems()).hasSize(3);
}
@Test
public void simpleEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(SimpleEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.simple")
.fromSource(SimpleEndpoint.class));
assertThat(metadata).has(enabledFlag("simple", true));
assertThat(metadata).has(jmxEnabledFlag("simple", true));
assertThat(metadata).has(webEnabledFlag("simple", true));
assertThat(metadata).has(cacheTtl("simple"));
assertThat(metadata.getItems()).hasSize(5);
}
@Test
public void disableEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(DisabledEndpoint.class);
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(cacheTtl("disabled"));
assertThat(metadata.getItems()).hasSize(5);
}
@Test
public void customPropertiesEndpoint() throws IOException {
ConfigurationMetadata metadata = compile(CustomPropertiesEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.customprops")
.fromSource(CustomPropertiesEndpoint.class));
assertThat(metadata).has(Metadata.withProperty("endpoints.customprops.name").
ofType(String.class).withDefaultValue("test"));
assertThat(metadata).has(enabledFlag("customprops", true));
assertThat(metadata).has(jmxEnabledFlag("customprops", true));
assertThat(metadata).has(webEnabledFlag("customprops", true));
assertThat(metadata).has(cacheTtl("customprops"));
assertThat(metadata.getItems()).hasSize(6);
}
@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", true));
assertThat(metadata).has(jmxEnabledFlag("jmx", true));
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", true));
assertThat(metadata).has(webEnabledFlag("web", true));
assertThat(metadata).has(cacheTtl("web"));
assertThat(metadata.getItems()).hasSize(4);
}
@Test
public void incrementalEndpointBuildChangeGeneralEnabledFlag() throws Exception {
TestProject project = new TestProject(this.temporaryFolder,
IncrementalEndpoint.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", true));
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
assertThat(metadata).has(webEnabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", enabledByDefault = 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(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
}
@Test
public void incrementalEndpointBuildDisableJmxEndpoint() throws Exception {
TestProject project = new TestProject(this.temporaryFolder,
IncrementalEndpoint.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", true));
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
assertThat(metadata).has(webEnabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", types = Endpoint.Type.WEB");
metadata = project.incrementalBuild(IncrementalEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", true));
assertThat(metadata).has(webEnabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(4);
}
@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", true));
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(4);
project.replaceText(IncrementalJmxEndpoint.class, ", types = Endpoint.Type.JMX",
"");
metadata = project.incrementalBuild(IncrementalJmxEndpoint.class);
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
.fromSource(IncrementalJmxEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", true));
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
assertThat(metadata).has(webEnabledFlag("incremental", true));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
}
private Metadata.MetadataItemCondition enabledFlag(String endpointId,
boolean defaultValue) {
return Metadata.withEnabledFlag("endpoints." + endpointId + ".enabled")
.withDefaultValue(defaultValue).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 cacheTtl(String endpointId) {
return Metadata.withProperty("endpoints." + endpointId + ".cache.time-to-live")
.ofType(Long.class).withDefaultValue(0).withDescription(
"Maximum time in milliseconds that a response can be cached.");
}
@Test
public void mergingOfAdditionalProperty() throws Exception {
ItemMetadata property = ItemMetadata.newProperty(null, "foo", "java.lang.String",

View File

@@ -46,6 +46,8 @@ public class TestConfigurationMetadataAnnotationProcessor
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.DeprecatedConfigurationProperty";
static final String ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.Endpoint";
private ConfigurationMetadata metadata;
private final File outputLocation;
@@ -69,6 +71,11 @@ public class TestConfigurationMetadataAnnotationProcessor
return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION;
}
@Override
protected String endpointAnnotation() {
return ENDPOINT_ANNOTATION;
}
@Override
protected ConfigurationMetadata writeMetaData() throws Exception {
super.writeMetaData();

View File

@@ -62,6 +62,10 @@ public final class Metadata {
return new MetadataItemCondition(ItemType.PROPERTY, name).ofType(type);
}
public static Metadata.MetadataItemCondition withEnabledFlag(String key) {
return withProperty(key).ofType(Boolean.class);
}
public static MetadataHintCondition withHint(String name) {
return new MetadataHintCondition(name);
}

View File

@@ -0,0 +1,50 @@
/*
* 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;
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;
/**
* Alternative to Spring Boot's {@code @Endpoint} for testing (removes the need for a
* dependency on the real annotation).
*
* @author Stephane Nicoll
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Endpoint {
String id();
boolean enabledByDefault() default true;
Type[] types() default {};
enum Type {
JMX,
WEB
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.ConfigurationProperties;
import org.springframework.boot.configurationsample.Endpoint;
/**
* An endpoint with additional custom properties.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "customprops")
@ConfigurationProperties("endpoints.customprops")
public class CustomPropertiesEndpoint {
private String name = "test";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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;
/**
* An endpoint that is disabled by default.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "disabled", enabledByDefault = false)
public class DisabledEndpoint {
}

View File

@@ -0,0 +1,29 @@
/*
* 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;
/**
* An endpoint that only exposes a JMX MBean.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "jmx", types = Endpoint.Type.JMX)
public class OnlyJmxEndpoint {
}

View File

@@ -0,0 +1,29 @@
/*
* 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;
/**
* An endpoints that only exposes a web endpoint.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "web", types = Endpoint.Type.WEB)
public class OnlyWebEndpoint {
}

View File

@@ -0,0 +1,29 @@
/*
* 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;
/**
* A simple endpoint with no default override.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "simple")
public class SimpleEndpoint {
}

View File

@@ -0,0 +1,29 @@
/*
* 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.incremental;
import org.springframework.boot.configurationsample.Endpoint;
/**
* An endpoint that is enabled by default.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "incremental")
public class IncrementalEndpoint {
}

View File

@@ -0,0 +1,29 @@
/*
* 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.incremental;
import org.springframework.boot.configurationsample.Endpoint;
/**
* An endpoint that only exposes a JMX MBean.
*
* @author Stephane Nicoll
*/
@Endpoint(id = "incremental", types = Endpoint.Type.JMX)
public class IncrementalJmxEndpoint {
}