Load auto-configurations from META-INF/spring-boot

Implements a new AutoConfigurationLoader, which loads
auto-configurations from a file in META-INF/spring-boot.

Adapts the AutoConfigurationImportSelector to use the new loader.

Adapts the ImportAutoConfigurationImportSelector to use the new loader.

Adapts the metadata plugin in the build to additionally load the
auto-configurations from the new file.

Updates the documentation for auto-configurations and test slices.

Closes gh-29872
This commit is contained in:
Moritz Halbritter 2022-02-17 09:10:53 +01:00
parent a55759d531
commit 7d68b58c8f
17 changed files with 292 additions and 28 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* 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.
@ -16,14 +16,19 @@
package org.springframework.boot.build.autoconfigure;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
@ -48,6 +53,8 @@ import org.springframework.util.StringUtils;
*/
public class AutoConfigurationMetadata extends DefaultTask {
private static final String COMMENT_START = "#";
private SourceSet sourceSet;
private File outputFile;
@ -57,6 +64,12 @@ public class AutoConfigurationMetadata extends DefaultTask {
.file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
"META-INF/spring.factories"))
.withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("spring.factories");
getInputs()
.file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
"META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration"))
.withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("org.springframework.boot.autoconfigure.AutoConfiguration");
dependsOn((Callable<String>) () -> this.sourceSet.getProcessResourcesTaskName());
getProject().getConfigurations()
.maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME);
@ -86,11 +99,9 @@ public class AutoConfigurationMetadata extends DefaultTask {
private Properties readAutoConfiguration() throws IOException {
Properties autoConfiguration = CollectionFactory.createSortedProperties(true);
Properties springFactories = readSpringFactories(
new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
String enableAutoConfiguration = springFactories
.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
Set<String> classNames = StringUtils.commaDelimitedListToSet(enableAutoConfiguration);
Set<String> classNames = new LinkedHashSet<>();
classNames.addAll(readSpringFactories());
classNames.addAll(readAutoConfigurationsFile());
Set<String> publicClassNames = new LinkedHashSet<>();
for (String className : classNames) {
File classFile = findClassFile(className);
@ -109,6 +120,57 @@ public class AutoConfigurationMetadata extends DefaultTask {
return autoConfiguration;
}
/**
* Reads auto-configurations from META-INF/spring.factories.
* @return auto-configurations
*/
private Set<String> readSpringFactories() throws IOException {
File file = new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories");
if (!file.exists()) {
return Collections.emptySet();
}
Properties springFactories = readSpringFactories(file);
String enableAutoConfiguration = springFactories
.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
return StringUtils.commaDelimitedListToSet(enableAutoConfiguration);
}
/**
* Reads auto-configurations from
* META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration.
* @return auto-configurations
*/
private List<String> readAutoConfigurationsFile() throws IOException {
File file = new File(this.sourceSet.getOutput().getResourcesDir(),
"META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration");
if (!file.exists()) {
return Collections.emptyList();
}
// Nearly identical copy of
// org.springframework.boot.autoconfigure.AutoConfigurationLoader.readAutoConfigurations
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
List<String> autoConfigurations = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
line = stripComment(line);
line = line.trim();
if (line.isEmpty()) {
continue;
}
autoConfigurations.add(line);
}
return autoConfigurations;
}
}
private String stripComment(String line) {
int commentStart = line.indexOf(COMMENT_START);
if (commentStart == -1) {
return line;
}
return line.substring(0, commentStart);
}
private File findClassFile(String className) {
String classFileName = className.replace(".", "/") + ".class";
for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) {

View File

@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Indicates that a class provides configuration that can be automatically applied by
@ -33,6 +34,10 @@ import org.springframework.context.annotation.Configuration;
* {@link Configuration @Configuration} with the exception that
* {@literal Configuration#proxyBeanMethods() proxyBeanMethods} is always {@code false}.
* <p>
* They are located using the {@link AutoConfigurationLoader} and the
* {@link SpringFactoriesLoader} mechanism (keyed against
* {@link EnableAutoConfiguration}).
* <p>
* Generally auto-configuration classes are marked as {@link Conditional @Conditional}
* (most often using {@link ConditionalOnClass @ConditionalOnClass} and
* {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.BeanClassLoaderAware;
@ -54,13 +55,20 @@ public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoad
}
private boolean isAutoConfiguration(MetadataReader metadataReader) {
return getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata()
.isAnnotated(AutoConfiguration.class.getName());
return annotatedWithAutoConfiguration
|| getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
}
protected List<String> getAutoConfigurations() {
if (this.autoConfigurations == null) {
this.autoConfigurations = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
this.beanClassLoader);
List<String> autoConfigurations = new ArrayList<>();
autoConfigurations.addAll(
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader));
autoConfigurations
.addAll(new AutoConfigurationLoader().loadNames(AutoConfiguration.class, this.beanClassLoader));
this.autoConfigurations = autoConfigurations;
}
return this.autoConfigurations;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@ -67,6 +67,7 @@ import org.springframework.util.StringUtils;
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Moritz Halbritter
* @since 1.3.0
* @see EnableAutoConfiguration
*/
@ -167,7 +168,9 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* this method will load candidates using {@link AutoConfigurationLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}. For backward compatible reasons it
* will also consider {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
@ -175,10 +178,13 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector,
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
List<String> configurations = new ArrayList<>();
configurations.addAll(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
configurations.addAll(new AutoConfigurationLoader().loadNames(AutoConfiguration.class, getBeanClassLoader()));
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

View File

@ -0,0 +1,113 @@
/*
* 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.autoconfigure;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
/**
* Loads the names of annotated classes, usually @{@link AutoConfiguration}.
*
* The names of the classes are stored in files named META-INF/spring-boot/{full qualified
* name of the annotation}. Every line contains the full qualified class name of the
* annotated class. Comments are supported using the # character.
*
* @author Moritz Halbritter
* @see AutoConfiguration
* @see SpringFactoriesLoader
*/
class AutoConfigurationLoader {
private static final String LOCATION = "META-INF/spring-boot/";
private static final String COMMENT_START = "#";
/**
* Loads the names of annotated classes.
* @param annotation annotation to load
* @param classLoader class loader to use for loading
* @return list of names of annotated classes
*/
List<String> loadNames(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = LOCATION + annotation.getName();
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> autoConfigurations = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
autoConfigurations.addAll(readAutoConfigurations(url));
}
return autoConfigurations;
}
private ClassLoader decideClassloader(ClassLoader classLoader) {
if (classLoader == null) {
return AutoConfigurationLoader.class.getClassLoader();
}
return classLoader;
}
private Enumeration<URL> findUrlsInClasspath(ClassLoader classLoader, String location) {
try {
return classLoader.getResources(location);
}
catch (IOException ex) {
throw new IllegalArgumentException("Failed to load autoconfigurations from location [" + location + "]",
ex);
}
}
private List<String> readAutoConfigurations(URL url) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
List<String> autoConfigurations = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
line = stripComment(line);
line = line.trim();
if (line.isEmpty()) {
continue;
}
autoConfigurations.add(line);
}
return autoConfigurations;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load autoconfigurations from location [" + url + "]", ex);
}
}
private String stripComment(String line) {
int commentStart = line.indexOf(COMMENT_START);
if (commentStart == -1) {
return line;
}
return line.substring(0, commentStart);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@ -60,9 +60,9 @@ import org.springframework.core.io.support.SpringFactoriesLoader;
* and classes can be searched.
* <p>
* Auto-configuration classes are regular Spring {@link Configuration @Configuration}
* beans. They are located using the {@link SpringFactoriesLoader} mechanism (keyed
* against this class). Generally auto-configuration beans are
* {@link Conditional @Conditional} beans (most often using
* beans. They are located using the {@link AutoConfigurationLoader} and the
* {@link SpringFactoriesLoader} mechanism (keyed against this class). Generally
* auto-configuration beans are {@link Conditional @Conditional} beans (most often using
* {@link ConditionalOnClass @ConditionalOnClass} and
* {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
*

View File

@ -44,6 +44,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector implements DeterminableImports {
@ -94,7 +95,10 @@ class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelec
}
protected Collection<String> loadFactoryNames(Class<?> source) {
return SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader());
List<String> factoryNames = new ArrayList<>();
factoryNames.addAll(SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader()));
factoryNames.addAll(new AutoConfigurationLoader().loadNames(source, getBeanClassLoader()));
return factoryNames;
}
@Override

View File

@ -0,0 +1,53 @@
/*
* 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.autoconfigure;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AutoConfigurationLoaderTests {
private AutoConfigurationLoader sut;
@BeforeEach
void setUp() {
this.sut = new AutoConfigurationLoader();
}
@Test
void loadNames() {
List<String> classNames = this.sut.loadNames(TestAutoConfiguration.class, null);
assertThat(classNames).containsExactly("class1", "class2", "class3");
}
@AutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAutoConfiguration {
}
}

View File

@ -0,0 +1,6 @@
# A comment spanning a complete line
class1
class2 # with comment at the end
# Comment with some whitespace in front
class3

View File

@ -24,16 +24,18 @@ You can browse the source code of {spring-boot-autoconfigure-module-code}[`sprin
[[features.developing-auto-configuration.locating-auto-configuration-candidates]]
=== Locating Auto-configuration Candidates
Spring Boot checks for the presence of a `META-INF/spring.factories` file within your published jar.
The file should list your configuration classes under the `EnableAutoConfiguration` key, as shown in the following example:
Spring Boot checks for the presence of a `META-INF/spring-boot/org.springframework.boot.autoconfigure.AutoConfiguration` file within your published jar.
The file should list your configuration classes, as shown in the following example:
[indent=0]
----
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
----
TIP: You can use comments via `#` in this file.
NOTE: Auto-configurations must be loaded that way _only_.
Make sure that they are defined in a specific package space and that they are never the target of component scanning.
Furthermore, auto-configuration classes should not enable component scanning to find additional components.

View File

@ -769,13 +769,18 @@ include::code:MyJdbcTests[]
NOTE: Make sure to not use the regular `@Import` annotation to import auto-configurations as they are handled in a specific way by Spring Boot.
Alternatively, additional auto-configurations can be added for any use of a slice annotation by registering them in `META-INF/spring.factories` as shown in the following example:
Alternatively, additional auto-configurations can be added for any use of a slice annotation by registering them in a file stored in `META-INF/spring-boot` as shown in the following example:
.META-INF/spring-boot/org.springframework.boot.test.autoconfigure.jdbc.JdbcTest
[indent=0]
----
org.springframework.boot.test.autoconfigure.jdbc.JdbcTest=com.example.IntegrationAutoConfiguration
com.example.IntegrationAutoConfiguration
----
In this example, the `com.example.IntegrationAutoConfiguration` is enabled on every test annotated with `@JdbcTest`.
TIP: You can use comments via `#` in this file.
TIP: A slice or `@AutoConfigure...` annotation can be customized this way as long as it is meta-annotated with `@ImportAutoConfiguration`.