Add ability to ignore configuration properties

Properties which should be ignored can be specified in the
additional-spring-configuration-metadata.json file. The ignored
properties section is copied into the final
spring-configuration-metadata.json file, and the ignored properties are
removed from the properties element in the final file.

Closes gh-2421
This commit is contained in:
Moritz Halbritter 2025-01-21 15:21:44 +01:00
parent 08e9c16f33
commit f24ba9935c
11 changed files with 375 additions and 16 deletions

View File

@ -160,12 +160,14 @@ TIP: This has no effect on collections and maps, as those types are automaticall
== Adding Additional Metadata
Spring Boot's configuration file handling is quite flexible, and it is often the case that properties may exist that are not bound to a javadoc:org.springframework.boot.context.properties.ConfigurationProperties[format=annotation] bean.
You may also need to tune some attributes of an existing key.
You may also need to tune some attributes of an existing key or to ignore the key altogether.
To support such cases and let you provide custom "hints", the annotation processor automatically merges items from `META-INF/additional-spring-configuration-metadata.json` into the main metadata file.
If you refer to a property that has been detected automatically, the description, default value, and deprecation information are overridden, if specified.
If the manual property declaration is not identified in the current module, it is added as a new property.
The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`.
The items contained in the "`ignored.properties`" section are removed from the "`properties`" section of the generated `spring-configuration-metadata.json` file.
The additional properties file is optional.
If you do not have any additional properties, do not add the file.

View File

@ -2,7 +2,7 @@
= Metadata Format
Configuration metadata files are located inside jars under `META-INF/spring-configuration-metadata.json`.
They use a JSON format with items categorized under either "`groups`" or "`properties`" and additional values hints categorized under "hints", as shown in the following example:
They use a JSON format with items categorized under either "`groups`" or "`properties`", additional values hints categorized under "hints", and ignored items under "`ignored`" as shown in the following example:
[source,json]
----
@ -63,7 +63,15 @@ They use a JSON format with items categorized under either "`groups`" or "`prope
}
]
}
]}
...
],"ignored": {
"properties": [
{
"name": "server.ignored"
}
...
]
}}
----
Each "`property`" is a configuration item that the user specifies with a given value.
@ -82,9 +90,12 @@ For example, the `server.port` and `server.address` properties are part of the `
NOTE: It is not required that every "`property`" has a "`group`".
Some properties might exist in their own right.
Finally, "`hints`" are additional information used to assist the user in configuring a given property.
The "`hints`" are additional information used to assist the user in configuring a given property.
For example, when a developer is configuring the configprop:spring.jpa.hibernate.ddl-auto[] property, a tool can use the hints to offer some auto-completion help for the `none`, `validate`, `update`, `create`, and `create-drop` values.
Finally, "`ignored`" are items which have been deliberately ignored.
The content of this section usually comes from the xref:specification:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[additional metadata].
[[appendix.configuration-metadata.format.group]]
@ -292,6 +303,36 @@ The JSON object contained in the `providers` attribute of each `hint` element ca
[[appendix.configuration-metadata.format.ignored]]
== Ignored Attributes
The `ignored` object can contain the attributes shown in the following table:
[cols="1,1,4"]
|===
| Name | Type | Purpose
| `properties`
| IgnoredProperty[]
| A list of ignored properties as defined by the IgnoredProperty object (described in the next table). Each entry defines the name of the ignored property.
|===
The JSON object contained in the `properties` attribute of each `ignored` element can contain the attributes described in the following table:
[cols="1,1,4"]
|===
| Name | Type | Purpose
| `name`
| String
| The full name of the property to ignore.
Names are in lower-case period-separated form (such as `spring.mvc.servlet.path`).
This attribute is mandatory.
|===
[[appendix.configuration-metadata.format.repeated-items]]
== Repeated Metadata Items

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -48,6 +48,7 @@ import javax.tools.Diagnostic.Kind;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.InvalidConfigurationMetadataException;
import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation;
import org.springframework.boot.configurationprocessor.metadata.ItemIgnore;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
@ -372,6 +373,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
protected ConfigurationMetadata writeMetadata() throws Exception {
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
metadata = mergeAdditionalMetadata(metadata);
removeIgnored(metadata);
if (!metadata.getItems().isEmpty()) {
this.metadataStore.writeMetadata(metadata);
return metadata;
@ -379,6 +381,12 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return null;
}
private void removeIgnored(ConfigurationMetadata metadata) {
for (ItemIgnore itemIgnore : metadata.getIgnored()) {
metadata.removeMetadata(itemIgnore.getType(), itemIgnore.getName());
}
}
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) {
try {
ConfigurationMetadata merged = new ConfigurationMetadata(metadata);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType;
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
/**
@ -29,6 +30,7 @@ import org.springframework.boot.configurationprocessor.support.ConventionUtils;
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Moritz Halbritter
* @since 1.2.0
* @see ItemMetadata
*/
@ -38,14 +40,18 @@ public class ConfigurationMetadata {
private final Map<String, List<ItemHint>> hints;
private final Map<String, List<ItemIgnore>> ignored;
public ConfigurationMetadata() {
this.items = new LinkedHashMap<>();
this.hints = new LinkedHashMap<>();
this.ignored = new LinkedHashMap<>();
}
public ConfigurationMetadata(ConfigurationMetadata metadata) {
this.items = new LinkedHashMap<>(metadata.items);
this.hints = new LinkedHashMap<>(metadata.hints);
this.ignored = new LinkedHashMap<>(metadata.ignored);
}
/**
@ -73,6 +79,32 @@ public class ConfigurationMetadata {
add(this.hints, itemHint.getName(), itemHint, false);
}
/**
* Add item ignore.
* @param itemIgnore the item ignore to add
* @since 3.5.0
*/
public void add(ItemIgnore itemIgnore) {
add(this.ignored, itemIgnore.getName(), itemIgnore, false);
}
/**
* Remove item meta-data for the given item type and name.
* @param itemType the item type
* @param name the name
* @since 3.5.0
*/
public void removeMetadata(ItemType itemType, String name) {
List<ItemMetadata> metadata = this.items.get(name);
if (metadata == null) {
return;
}
metadata.removeIf((item) -> item.isOfItemType(itemType));
if (metadata.isEmpty()) {
this.items.remove(name);
}
}
/**
* Merge the content from another {@link ConfigurationMetadata}.
* @param metadata the {@link ConfigurationMetadata} instance to merge
@ -84,6 +116,9 @@ public class ConfigurationMetadata {
for (ItemHint itemHint : metadata.getHints()) {
add(itemHint);
}
for (ItemIgnore itemIgnore : metadata.getIgnored()) {
add(itemIgnore);
}
}
/**
@ -102,6 +137,14 @@ public class ConfigurationMetadata {
return flattenValues(this.hints);
}
/**
* Return ignore meta-data.
* @return the ignores
*/
public List<ItemIgnore> getIgnored() {
return flattenValues(this.ignored);
}
protected void mergeItemMetadata(ItemMetadata metadata) {
ItemMetadata matching = findMatchingItemMetadata(metadata);
if (matching != null) {

View File

@ -0,0 +1,87 @@
/*
* Copyright 2012-2025 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
*
* https://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.configurationprocessor.metadata;
import java.util.Objects;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType;
/**
* Ignored item.
*
* @author Moritz Halbritter
* @since 3.5.0
*/
public final class ItemIgnore implements Comparable<ItemIgnore> {
private final ItemType type;
private final String name;
private ItemIgnore(ItemType type, String name) {
if (name == null) {
throw new IllegalArgumentException("'name' must not be null");
}
if (type == null) {
throw new IllegalArgumentException("'type' must not be null");
}
this.type = type;
this.name = name;
}
public String getName() {
return this.name;
}
public ItemType getType() {
return this.type;
}
@Override
public int compareTo(ItemIgnore other) {
return getName().compareTo(other.getName());
}
/**
* Create an ignore for a property with the given name.
* @param name the name
* @return the item ignore
*/
public static ItemIgnore forProperty(String name) {
return new ItemIgnore(ItemType.PROPERTY, name);
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
ItemIgnore that = (ItemIgnore) o;
return this.type == that.type && Objects.equals(this.name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(this.type, this.name);
}
@Override
public String toString() {
return "ItemIgnore{" + "type=" + this.type + ", name='" + this.name + '\'' + '}';
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -31,6 +31,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.Ite
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Moritz Halbritter
*/
class JsonConverter {
@ -59,6 +60,21 @@ class JsonConverter {
return jsonArray;
}
JSONObject toJsonObject(Collection<ItemIgnore> ignored) throws Exception {
JSONObject result = new JSONObject();
result.put("properties", ignoreToJsonArray(
ignored.stream().filter((itemIgnore) -> itemIgnore.getType() == ItemType.PROPERTY).toList()));
return result;
}
private JSONArray ignoreToJsonArray(Collection<ItemIgnore> ignored) throws Exception {
JSONArray result = new JSONArray();
for (ItemIgnore itemIgnore : ignored) {
result.put(toJsonObject(itemIgnore));
}
return result;
}
JSONObject toJsonObject(ItemMetadata item) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", item.getName());
@ -103,6 +119,12 @@ class JsonConverter {
return jsonObject;
}
private JSONObject toJsonObject(ItemIgnore ignore) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", ignore.getName());
return jsonObject;
}
private JSONArray getItemHintValues(ItemHint hint) throws Exception {
JSONArray values = new JSONArray();
for (ItemHint.ValueHint value : hint.getValues()) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -30,6 +30,7 @@ import java.util.Set;
import java.util.TreeSet;
import org.springframework.boot.configurationprocessor.json.JSONArray;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType;
@ -50,6 +51,7 @@ public class JsonMarshaller {
object.put("groups", converter.toJsonArray(metadata, ItemType.GROUP));
object.put("properties", converter.toJsonArray(metadata, ItemType.PROPERTY));
object.put("hints", converter.toJsonArray(metadata.getHints()));
object.put("ignored", converter.toJsonObject(metadata.getIgnored()));
outputStream.write(object.toString(2).getBytes(StandardCharsets.UTF_8));
}
catch (Exception ex) {
@ -67,7 +69,7 @@ public class JsonMarshaller {
ConfigurationMetadata metadata = new ConfigurationMetadata();
JSONObject object = new JSONObject(toString(inputStream));
JsonPath path = JsonPath.root();
checkAllowedKeys(object, path, "groups", "properties", "hints");
checkAllowedKeys(object, path, "groups", "properties", "hints", "ignored");
JSONArray groups = object.optJSONArray("groups");
if (groups != null) {
for (int i = 0; i < groups.length(); i++) {
@ -88,9 +90,27 @@ public class JsonMarshaller {
metadata.add(toItemHint((JSONObject) hints.get(i), path.resolve("hints").index(i)));
}
}
JSONObject ignored = object.optJSONObject("ignored");
if (ignored != null) {
checkAllowedKeys(ignored, path.resolve("ignored"), "properties");
addIgnoredProperties(metadata, ignored, path.resolve("ignored"));
}
return metadata;
}
private void addIgnoredProperties(ConfigurationMetadata metadata, JSONObject ignored, JsonPath path)
throws JSONException {
JSONArray properties = ignored.optJSONArray("properties");
if (properties == null) {
return;
}
for (int i = 0; i < properties.length(); i++) {
JSONObject jsonObject = properties.getJSONObject(i);
checkAllowedKeys(jsonObject, path.resolve("properties").index(i), "name");
metadata.add(ItemIgnore.forProperty(jsonObject.getString("name")));
}
}
private ItemMetadata toItemMetadata(JSONObject object, JsonPath path, ItemType itemType) throws Exception {
switch (itemType) {
case GROUP -> checkAllowedKeys(object, path, "name", "type", "description", "sourceType", "sourceMethod");

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -22,6 +22,7 @@ import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemIgnore;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.Metadata;
import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration;
@ -38,6 +39,7 @@ import org.springframework.boot.configurationsample.simple.DescriptionProperties
import org.springframework.boot.configurationsample.simple.HierarchicalProperties;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesGrandparent;
import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesParent;
import org.springframework.boot.configurationsample.simple.IgnoredProperties;
import org.springframework.boot.configurationsample.simple.InnerClassWithPrivateConstructor;
import org.springframework.boot.configurationsample.simple.NotAnnotated;
import org.springframework.boot.configurationsample.simple.SimpleArrayProperties;
@ -570,4 +572,24 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
.withDescription("last description in Javadoc"));
}
@Test
void shouldIgnoreProperties() {
String additionalMetadata = """
{
"ignored": {
"properties": [
{
"name": "ignored.prop3"
}
]
}
}
""";
ConfigurationMetadata metadata = compile(additionalMetadata, IgnoredProperties.class);
assertThat(metadata).has(Metadata.withProperty("ignored.prop1", String.class));
assertThat(metadata).has(Metadata.withProperty("ignored.prop2", String.class));
assertThat(metadata).doesNotHave(Metadata.withProperty("ignored.prop3", String.class));
assertThat(metadata.getIgnored()).containsExactly(ItemIgnore.forProperty("ignored.prop3"));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatException;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
class JsonMarshallerTests {
@ -174,14 +175,36 @@ class JsonMarshallerTests {
"\"java.lang.Boolean\"", "\"com.example.bravo.aaa\"", "\"java.lang.Integer\"", "\"com.example.Bar");
}
@Test
void shouldReadIgnoredProperties() throws Exception {
String json = """
{
"ignored": {
"properties": [
{
"name": "prop1"
},
{
"name": "prop2"
}
]
}
}
""";
ConfigurationMetadata metadata = read(json);
assertThat(metadata.getIgnored()).containsExactly(ItemIgnore.forProperty("prop1"),
ItemIgnore.forProperty("prop2"));
}
@Test
void shouldCheckRootFields() {
String json = """
{
"groups": [], "properties": [], "hints": [], "dummy": []
"groups": [], "properties": [], "hints": [], "ignored": {}, "dummy": []
}""";
assertThatException().isThrownBy(() -> read(json))
.withMessage("Expected only keys [groups, hints, properties], but found additional keys [dummy]. Path: .");
.withMessage(
"Expected only keys [groups, hints, ignored, properties], but found additional keys [dummy]. Path: .");
}
@Test
@ -324,9 +347,41 @@ class JsonMarshallerTests {
"Expected only keys [name, parameters], but found additional keys [dummy]. Path: .hints.[0].providers.[0]");
}
private void read(String json) throws Exception {
@Test
void shouldCheckIgnoreFields() {
String json = """
{
"ignored": {
"properties": [],
"dummy": {}
}
}
""";
assertThatException().isThrownBy(() -> read(json))
.withMessage("Expected only keys [properties], but found additional keys [dummy]. Path: .ignored");
}
@Test
void shouldCheckIgnorePropertiesFields() {
String json = """
{
"ignored": {
"properties": [
{
"name": "prop1",
"dummy": true
}
]
}
}
""";
assertThatException().isThrownBy(() -> read(json))
.withMessage("Expected only keys [name], but found additional keys [dummy]. Path: .ignored.properties.[0]");
}
private ConfigurationMetadata read(String json) throws Exception {
JsonMarshaller marshaller = new JsonMarshaller();
marshaller.read(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
return marshaller.read(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2012-2025 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
*
* https://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.simple;
import org.springframework.boot.configurationsample.ConfigurationProperties;
/**
* Configuration properties where some of them are being ignored.
*
* @author Moritz Halbritter
*/
@ConfigurationProperties("ignored")
public class IgnoredProperties {
private String prop1;
private String prop2;
private String prop3;
public String getProp1() {
return this.prop1;
}
public void setProp1(String prop1) {
this.prop1 = prop1;
}
public String getProp2() {
return this.prop2;
}
public void setProp2(String prop2) {
this.prop2 = prop2;
}
public String getProp3() {
return this.prop3;
}
public void setProp3(String prop3) {
this.prop3 = prop3;
}
}