From d5695c19310aaaa61d78f9032b87f282a6d19b90 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 14 Jun 2022 10:14:31 +0200 Subject: [PATCH] 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 --- .../mock/MockSpringFactoriesLoader.java | 17 ++- ...nfigDataLocationRuntimeHintsRegistrar.java | 110 ++++++++++++++ .../StandardConfigDataLocationResolver.java | 2 +- .../resources/META-INF/spring/aot.factories | 2 + ...ataLocationRuntimeHintsRegistrarTests.java | 135 ++++++++++++++++++ 5 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrar.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrarTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/mock/MockSpringFactoriesLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/mock/MockSpringFactoriesLoader.java index 86a3dd9a3f4..372dd2231bc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/mock/MockSpringFactoriesLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/mock/MockSpringFactoriesLoader.java @@ -79,24 +79,28 @@ public class MockSpringFactoriesLoader extends SpringFactoriesLoader { * @param factoryType the factory type class * @param factoryImplementations the implementation classes * @param the factory type + * @return {@code this}, to facilitate method chaining */ @SafeVarargs - public final void add(Class factoryType, Class... factoryImplementations) { + public final MockSpringFactoriesLoader add(Class factoryType, Class... factoryImplementations) { for (Class 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 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 the factory type + * @return {@code this}, to facilitate method chaining */ @SuppressWarnings("unchecked") - public void addInstance(Class factoryType, T... factoryInstances) { - addInstance(factoryType.getName(), factoryInstances); + public MockSpringFactoriesLoader addInstance(Class 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 the factory instance type + * @return {@code this}, to facilitate method chaining */ @SuppressWarnings("unchecked") - public void addInstance(String factoryType, T... factoryInstance) { + public MockSpringFactoriesLoader addInstance(String factoryType, T... factoryInstance) { List 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; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrar.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrar.java new file mode 100644 index 00000000000..5c5f09daa1f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrar.java @@ -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 fileNames = getFileNames(classLoader); + List locations = getLocations(classLoader); + List 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 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 getLocations(ClassLoader classLoader) { + List 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 getExtensions(ClassLoader classLoader) { + List extensions = new ArrayList<>(); + List 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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index 10743184f3d..01b66aaaa98 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -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*]*?:)(.*$)"); diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index 47dee9feebf..354b591e16c 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrarTests.java new file mode 100644 index 00000000000..3192cf91ab8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHintsRegistrarTests.java @@ -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 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 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 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 getLocations(ClassLoader classLoader) { + return List.of(UUID.randomUUID().toString()); + } + }.registerHints(hints, null); + assertThat(hints.resources().resourcePatterns()).isEmpty(); + } + + private Consumer 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; + } + + } + +}