Add resource hints for configuration properties

This commits introduces a RuntimeHintsRegistrar for configuration
properties. By default, it provides the necessary hint to load
application properties and yaml files in default locations.

Closes gh-31311
pull/31400/head
Stephane Nicoll 2 years ago
parent 47b980a687
commit d5695c1931

@ -79,24 +79,28 @@ public class MockSpringFactoriesLoader extends SpringFactoriesLoader {
* @param factoryType the factory type class
* @param factoryImplementations the implementation classes
* @param <T> the factory type
* @return {@code this}, to facilitate method chaining
*/
@SafeVarargs
public final <T> void add(Class<T> factoryType, Class<? extends T>... factoryImplementations) {
public final <T> MockSpringFactoriesLoader add(Class<T> factoryType, Class<? extends T>... factoryImplementations) {
for (Class<? extends T> factoryImplementation : factoryImplementations) {
add(factoryType.getName(), factoryImplementation.getName());
}
return this;
}
/**
* Add factory implementations to this instance.
* @param factoryType the factory type class name
* @param factoryImplementations the implementation class names
* @return {@code this}, to facilitate method chaining
*/
public void add(String factoryType, String... factoryImplementations) {
public MockSpringFactoriesLoader add(String factoryType, String... factoryImplementations) {
List<String> implementations = this.factories.computeIfAbsent(factoryType, (key) -> new ArrayList<>());
for (String factoryImplementation : factoryImplementations) {
implementations.add(factoryImplementation);
}
return this;
}
/**
@ -104,10 +108,11 @@ public class MockSpringFactoriesLoader extends SpringFactoriesLoader {
* @param factoryType the factory type class
* @param factoryInstances the implementation instances to add
* @param <T> the factory type
* @return {@code this}, to facilitate method chaining
*/
@SuppressWarnings("unchecked")
public <T> void addInstance(Class<T> factoryType, T... factoryInstances) {
addInstance(factoryType.getName(), factoryInstances);
public <T> MockSpringFactoriesLoader addInstance(Class<T> factoryType, T... factoryInstances) {
return addInstance(factoryType.getName(), factoryInstances);
}
/**
@ -115,9 +120,10 @@ public class MockSpringFactoriesLoader extends SpringFactoriesLoader {
* @param factoryType the factory type class name
* @param factoryInstance the implementation instances to add
* @param <T> the factory instance type
* @return {@code this}, to facilitate method chaining
*/
@SuppressWarnings("unchecked")
public <T> void addInstance(String factoryType, T... factoryInstance) {
public <T> MockSpringFactoriesLoader addInstance(String factoryType, T... factoryInstance) {
List<String> implementations = this.factories.computeIfAbsent(factoryType, (key) -> new ArrayList<>());
for (T factoryImplementation : factoryInstance) {
String reference = "!" + factoryType + ":" + factoryImplementation.getClass().getName()
@ -125,6 +131,7 @@ public class MockSpringFactoriesLoader extends SpringFactoriesLoader {
implementations.add(reference);
this.implementations.put(reference, factoryImplementation);
}
return this;
}
}

@ -0,0 +1,110 @@
/*
* Copyright 2012-2022 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.context.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.support.FilePatternResourceHintsRegistrar;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.ResourceUtils;
/**
* {@link RuntimeHintsRegistrar} implementation for application configuration.
*
* @author Stephane Nicoll
* @since 3.0
* @see FilePatternResourceHintsRegistrar
*/
public class ConfigDataLocationRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
private static final Log logger = LogFactory.getLog(ConfigDataLocationRuntimeHintsRegistrar.class);
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
List<String> fileNames = getFileNames(classLoader);
List<String> locations = getLocations(classLoader);
List<String> extensions = getExtensions(classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Registering application configuration hints for " + fileNames + "(" + extensions + ") at "
+ locations);
}
new FilePatternResourceHintsRegistrar(fileNames, locations, extensions).registerHints(hints.resources(),
classLoader);
}
/**
* Get the application file names to consider.
* @param classLoader the classloader to use
* @return the configuration file names
*/
protected List<String> getFileNames(ClassLoader classLoader) {
return Arrays.asList(StandardConfigDataLocationResolver.DEFAULT_CONFIG_NAMES);
}
/**
* Get the locations to consider. A location is a classpath location that may or may
* not use the standard {@code classpath:} prefix.
* @param classLoader the classloader to use
* @return the configuration file locations
*/
protected List<String> getLocations(ClassLoader classLoader) {
List<String> classpathLocations = new ArrayList<>();
for (ConfigDataLocation candidate : ConfigDataEnvironment.DEFAULT_SEARCH_LOCATIONS) {
for (ConfigDataLocation configDataLocation : candidate.split()) {
String location = configDataLocation.getValue();
if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
classpathLocations.add(location);
}
}
}
return classpathLocations;
}
/**
* Get the application file extensions to consider. A valid extension starts with a
* dot.
* @param classLoader the classloader to use
* @return the configuration file extensions
*/
protected List<String> getExtensions(ClassLoader classLoader) {
List<String> extensions = new ArrayList<>();
List<PropertySourceLoader> propertySourceLoaders = getSpringFactoriesLoader(classLoader)
.load(PropertySourceLoader.class);
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
for (String fileExtension : propertySourceLoader.getFileExtensions()) {
String candidate = "." + fileExtension;
if (!extensions.contains(candidate)) {
extensions.add(candidate);
}
}
}
return extensions;
}
protected SpringFactoriesLoader getSpringFactoriesLoader(ClassLoader classLoader) {
return SpringFactoriesLoader.forDefaultResourceLocation(classLoader);
}
}

@ -62,7 +62,7 @@ public class StandardConfigDataLocationResolver
static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String[] DEFAULT_CONFIG_NAMES = { "application" };
static final String[] DEFAULT_CONFIG_NAMES = { "application" };
private static final Pattern URL_PREFIX = Pattern.compile("^([a-zA-Z][a-zA-Z0-9*]*?:)(.*$)");

@ -1,6 +1,8 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.context.config.ConfigDataLocationRuntimeHintsRegistrar,\
org.springframework.boot.logging.logback.LogbackRuntimeHintsRegistrar,\
org.springframework.boot.WebApplicationType.WebApplicationTypeRuntimeHintsRegistrar
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor

@ -0,0 +1,135 @@
/*
* Copyright 2012-2022 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.context.config;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.ResourcePatternHint;
import org.springframework.aot.hint.ResourcePatternHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.testsupport.mock.MockSpringFactoriesLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConfigDataLocationRuntimeHintsRegistrar}.
*
* @author Stephane Nicoll
*/
class ConfigDataLocationRuntimeHintsRegistrarTests {
@Test
void registerWithDefaultSettings() {
RuntimeHints hints = new RuntimeHints();
new TestConfigDataLocationRuntimeHintsRegistrar().registerHints(hints, null);
assertThat(hints.resources().resourcePatterns()).singleElement()
.satisfies(includes("application*.properties", "application*.xml", "application*.yaml",
"application*.yml", "config/application*.properties", "config/application*.xml",
"config/application*.yaml", "config/application*.yml"));
}
@Test
void registerWithCustomName() {
RuntimeHints hints = new RuntimeHints();
new TestConfigDataLocationRuntimeHintsRegistrar() {
@Override
protected List<String> getFileNames(ClassLoader classLoader) {
return List.of("test");
}
}.registerHints(hints, null);
assertThat(hints.resources().resourcePatterns()).singleElement()
.satisfies(includes("test*.properties", "test*.xml", "test*.yaml", "test*.yml",
"config/test*.properties", "config/test*.xml", "config/test*.yaml", "config/test*.yml"));
}
@Test
void registerWithCustomLocation() {
RuntimeHints hints = new RuntimeHints();
new TestConfigDataLocationRuntimeHintsRegistrar() {
@Override
protected List<String> getLocations(ClassLoader classLoader) {
return List.of("config/");
}
}.registerHints(hints, null);
assertThat(hints.resources().resourcePatterns()).singleElement()
.satisfies(includes("config/application*.properties", "config/application*.xml",
"config/application*.yaml", "config/application*.yml"));
}
@Test
void registerWithCustomExtension() {
RuntimeHints hints = new RuntimeHints();
new ConfigDataLocationRuntimeHintsRegistrar() {
@Override
protected List<String> getExtensions(ClassLoader classLoader) {
return List.of(".conf");
}
}.registerHints(hints, null);
assertThat(hints.resources().resourcePatterns()).singleElement()
.satisfies(includes("application*.conf", "config/application*.conf"));
}
@Test
void registerWithUnknownLocationDoesNotAddHint() {
RuntimeHints hints = new RuntimeHints();
new ConfigDataLocationRuntimeHintsRegistrar() {
@Override
protected List<String> getLocations(ClassLoader classLoader) {
return List.of(UUID.randomUUID().toString());
}
}.registerHints(hints, null);
assertThat(hints.resources().resourcePatterns()).isEmpty();
}
private Consumer<ResourcePatternHints> includes(String... patterns) {
return (hint) -> {
assertThat(hint.getIncludes().stream().map(ResourcePatternHint::getPattern))
.containsExactlyInAnyOrder(patterns);
assertThat(hint.getExcludes()).isEmpty();
};
}
static class TestConfigDataLocationRuntimeHintsRegistrar extends ConfigDataLocationRuntimeHintsRegistrar {
private final MockSpringFactoriesLoader springFactoriesLoader;
TestConfigDataLocationRuntimeHintsRegistrar(MockSpringFactoriesLoader springFactoriesLoader) {
this.springFactoriesLoader = springFactoriesLoader;
}
TestConfigDataLocationRuntimeHintsRegistrar() {
this(new MockSpringFactoriesLoader().add(PropertySourceLoader.class, PropertiesPropertySourceLoader.class,
YamlPropertySourceLoader.class));
}
@Override
protected SpringFactoriesLoader getSpringFactoriesLoader(ClassLoader classLoader) {
return this.springFactoriesLoader;
}
}
}
Loading…
Cancel
Save