diff --git a/core/src/main/java/org/springframework/plugin/core/config/PluginRegistriesBeanDefinitionRegistrar.java b/core/src/main/java/org/springframework/plugin/core/config/PluginRegistriesBeanDefinitionRegistrar.java
index 2876549..3b98c07 100644
--- a/core/src/main/java/org/springframework/plugin/core/config/PluginRegistriesBeanDefinitionRegistrar.java
+++ b/core/src/main/java/org/springframework/plugin/core/config/PluginRegistriesBeanDefinitionRegistrar.java
@@ -29,6 +29,7 @@
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.Plugin;
+import org.springframework.plugin.core.PluginRegistry;
import org.springframework.plugin.core.support.PluginRegistryFactoryBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -39,6 +40,7 @@
* to the bean definition for the factory.
*
* @author Oliver Gierke
+ * @author Gabriel Hall
*/
public class PluginRegistriesBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@@ -67,36 +69,46 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
}
for (Class> type : types) {
+ registerPluginRegistry(type, registry);
+ }
+ }
- RootBeanDefinition beanDefinition = new RootBeanDefinition(PluginRegistryFactoryBean.class);
- beanDefinition.setTargetType(getTargetType(type, OrderAwarePluginRegistry.class));
- beanDefinition.getPropertyValues().addPropertyValue("type", type);
-
- Qualifier annotation = type.getAnnotation(Qualifier.class);
+ /**
+ * Registers a {@link PluginRegistryFactoryBean} for the given plugin type.
+ *
+ * @param type the plugin type to register.
+ * @param registry the bean definition registry.
+ */
+ public static void registerPluginRegistry(Class> type, BeanDefinitionRegistry registry) {
- // If the plugin interface has a Qualifier annotation, propagate that to the bean definition of the registry
- if (annotation != null) {
- AutowireCandidateQualifier qualifierMetadata = new AutowireCandidateQualifier(Qualifier.class);
- qualifierMetadata.setAttribute(AutowireCandidateQualifier.VALUE_KEY, annotation.value());
- beanDefinition.addQualifier(qualifierMetadata);
- }
+ RootBeanDefinition beanDefinition = new RootBeanDefinition(PluginRegistryFactoryBean.class);
+ beanDefinition.setTargetType(getTargetType(type, OrderAwarePluginRegistry.class));
+ beanDefinition.getPropertyValues().addPropertyValue("type", type);
- // Default
- String beanName = annotation == null //
- ? StringUtils.uncapitalize(type.getSimpleName() + "Registry") //
- : annotation.value();
+ Qualifier annotation = type.getAnnotation(Qualifier.class);
- registry.registerBeanDefinition(beanName, beanDefinition);
+ // If the plugin interface has a Qualifier annotation, propagate that to the bean definition of the registry
+ if (annotation != null) {
+ AutowireCandidateQualifier qualifierMetadata = new AutowireCandidateQualifier(Qualifier.class);
+ qualifierMetadata.setAttribute(AutowireCandidateQualifier.VALUE_KEY, annotation.value());
+ beanDefinition.addQualifier(qualifierMetadata);
}
+
+ String beanName = annotation == null //
+ ? StringUtils.uncapitalize(type.getSimpleName() + "Registry") //
+ : annotation.value();
+
+ registry.registerBeanDefinition(beanName, beanDefinition);
}
/**
* Returns the target type of the {@link PluginRegistry} for the given plugin type.
*
- * @param pluginType must not be {@literal null}.
- * @return
+ * @param pluginClass must not be {@literal null}.
+ * @param wrapper the registry implementation type.
+ * @return the registry target type.
*/
- private static ResolvableType getTargetType(Class> pluginClass, Class> wrapper) {
+ public static ResolvableType getTargetType(Class> pluginClass, Class> wrapper) {
Assert.notNull(pluginClass, "Plugin type must not be null!");
diff --git a/pom.xml b/pom.xml
index 44fe165..9a9f9f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
core
+ spring-plugin-autoconfigure
@@ -56,6 +57,7 @@
2.0.18
3.7.0
+ 4.2.0-M1
spring.plugin
diff --git a/spring-plugin-autoconfigure/pom.xml b/spring-plugin-autoconfigure/pom.xml
new file mode 100644
index 0000000..b3f560a
--- /dev/null
+++ b/spring-plugin-autoconfigure/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ spring-plugin-autoconfigure
+
+ Spring Plugin - Autoconfigure
+ Spring Boot auto-configuration for Spring Plugin
+
+
+ org.springframework.plugin
+ spring-plugin
+ 4.2.0-SNAPSHOT
+
+
+
+ spring.plugin.autoconfigure
+
+
+
+
+
+ src/main/resources
+ true
+
+
+ ../src/main/resources
+ true
+ META-INF
+
+
+
+
+
+
+
+ org.springframework.plugin
+ spring-plugin-core
+ ${project.version}
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+ ${spring-boot.version}
+
+
+
+ org.springframework.boot
+ spring-boot-test
+ ${spring-boot.version}
+ test
+
+
+
+
+
diff --git a/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfiguration.java b/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfiguration.java
new file mode 100644
index 0000000..b7befd6
--- /dev/null
+++ b/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfiguration.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2026 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.plugin.autoconfigure;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.ResolvableType;
+import org.springframework.plugin.core.Plugin;
+import org.springframework.plugin.core.PluginRegistry;
+import org.springframework.plugin.core.config.PluginRegistriesBeanDefinitionRegistrar;
+
+/**
+ * Registers plugin registries for direct plugin interfaces implemented by Spring beans.
+ *
+ * @author Gabriel Hall
+ */
+@AutoConfiguration
+public final class PluginRegistriesAutoConfiguration {
+
+ @Bean
+ static PluginRegistryBeanDefinitionRegistrar pluginRegistryBeanDefinitionRegistrar() {
+ return new PluginRegistryBeanDefinitionRegistrar();
+ }
+
+ static final class PluginRegistryBeanDefinitionRegistrar
+ implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
+
+ private @Nullable ListableBeanFactory beanFactory;
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+
+ ListableBeanFactory beanFactory = getBeanFactory();
+
+ for (Class> pluginType : getPluginTypes(beanFactory)) {
+ ResolvableType registryType = PluginRegistriesBeanDefinitionRegistrar.getTargetType(pluginType,
+ PluginRegistry.class);
+
+ if (beanFactory.getBeanNamesForType(registryType, true, false).length == 0) {
+ PluginRegistriesBeanDefinitionRegistrar.registerPluginRegistry(pluginType, registry);
+ }
+ }
+ }
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = (ListableBeanFactory) beanFactory;
+ }
+
+ private static Set> getPluginTypes(ListableBeanFactory beanFactory) {
+
+ Set> result = new LinkedHashSet<>();
+
+ for (String beanName : beanFactory.getBeanNamesForType(Plugin.class, true, false)) {
+
+ Class> beanType = beanFactory.getType(beanName, false);
+
+ if (beanType != null) {
+ collectPluginTypes(beanType, result);
+ }
+ }
+
+ return result;
+ }
+
+ private ListableBeanFactory getBeanFactory() {
+
+ if (this.beanFactory == null) {
+ throw new IllegalStateException("No ListableBeanFactory configured!");
+ }
+
+ return this.beanFactory;
+ }
+
+ private static void collectPluginTypes(Class> type, Set> result) {
+
+ if (type.isInterface() && type != Plugin.class && Plugin.class.isAssignableFrom(type)) {
+ result.add(type);
+ return;
+ }
+
+ for (Class> candidate : type.getInterfaces()) {
+
+ if (candidate != Plugin.class && Plugin.class.isAssignableFrom(candidate)) {
+ result.add(candidate);
+ }
+ }
+
+ Class> superclass = type.getSuperclass();
+
+ if (superclass != null && superclass != Object.class) {
+ collectPluginTypes(superclass, result);
+ }
+ }
+ }
+}
diff --git a/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/package-info.java b/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/package-info.java
new file mode 100644
index 0000000..eac15ab
--- /dev/null
+++ b/spring-plugin-autoconfigure/src/main/java/org/springframework/plugin/autoconfigure/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Spring Boot auto-configuration for plugin registries.
+ */
+@org.jspecify.annotations.NullMarked
+package org.springframework.plugin.autoconfigure;
diff --git a/spring-plugin-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-plugin-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..3453e34
--- /dev/null
+++ b/spring-plugin-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.plugin.autoconfigure.PluginRegistriesAutoConfiguration
diff --git a/spring-plugin-autoconfigure/src/test/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfigurationTests.java b/spring-plugin-autoconfigure/src/test/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfigurationTests.java
new file mode 100644
index 0000000..0c14bc8
--- /dev/null
+++ b/spring-plugin-autoconfigure/src/test/java/org/springframework/plugin/autoconfigure/PluginRegistriesAutoConfigurationTests.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2026 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.plugin.autoconfigure;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.plugin.core.Plugin;
+import org.springframework.plugin.core.PluginRegistry;
+import org.springframework.stereotype.Component;
+
+/**
+ * Tests for {@link PluginRegistriesAutoConfiguration}.
+ *
+ * @author Gabriel Hall
+ */
+class PluginRegistriesAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(PluginRegistriesAutoConfiguration.class));
+
+ @Test
+ void registersRegistryForPluginBean() {
+
+ this.contextRunner.withUserConfiguration(ComponentScanConfiguration.class).run(context -> {
+
+ PluginRegistry registry = context.getBean("samplePluginRegistry", PluginRegistry.class);
+
+ assertThat(registry).contains(context.getBean(SamplePlugin.class));
+ });
+ }
+
+ @Test
+ void registersRegistryForDeclaredPluginType() {
+
+ this.contextRunner.withUserConfiguration(DeclaredPluginConfiguration.class).run(context -> {
+
+ PluginRegistry registry = context.getBean("declaredPluginRegistry",
+ PluginRegistry.class);
+
+ assertThat(registry).contains(context.getBean(DeclaredPlugin.class));
+ });
+ }
+
+ @Test
+ void preservesExplicitRegistry() {
+
+ this.contextRunner.withUserConfiguration(ExplicitRegistryConfiguration.class).run(context -> {
+
+ PluginRegistry registry = context.getBean("customRegistry", PluginRegistry.class);
+
+ assertThat(registry.countPlugins()).isZero();
+ assertThat(context).doesNotHaveBean("samplePluginRegistry");
+ });
+ }
+
+ @Test
+ void usesPluginInterfaceQualifier() {
+
+ this.contextRunner.withUserConfiguration(QualifiedPluginConfiguration.class).run(context -> {
+
+ PluginRegistry registry = context.getBean("qualifiedRegistry", PluginRegistry.class);
+
+ assertThat(registry).contains(context.getBean(QualifiedPlugin.class));
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ComponentScan(basePackageClasses = SamplePluginImplementation.class, useDefaultFilters = false, includeFilters = @ComponentScan.Filter(
+ type = FilterType.ASSIGNABLE_TYPE, classes = SamplePluginImplementation.class))
+ static class ComponentScanConfiguration {}
+
+ @Configuration(proxyBeanMethods = false)
+ static class ExplicitRegistryConfiguration {
+
+ @Bean
+ SamplePluginImplementation samplePlugin() {
+ return new SamplePluginImplementation();
+ }
+
+ @Bean
+ PluginRegistry customRegistry() {
+ return PluginRegistry.empty();
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class QualifiedPluginConfiguration {
+
+ @Bean
+ QualifiedPluginImplementation qualifiedPlugin() {
+ return new QualifiedPluginImplementation();
+ }
+ }
+
+ interface SamplePlugin extends Plugin {}
+
+ @Qualifier("qualifiedRegistry")
+ interface QualifiedPlugin extends Plugin {}
+
+ interface DeclaredPlugin extends Plugin {}
+
+ @Configuration(proxyBeanMethods = false)
+ static class DeclaredPluginConfiguration {
+
+ @Bean
+ DeclaredPlugin declaredPlugin() {
+ return delimiter -> true;
+ }
+ }
+
+ @Component
+ static class SamplePluginImplementation implements SamplePlugin {
+
+ @Override
+ public boolean supports(String delimiter) {
+ return true;
+ }
+ }
+
+ static class QualifiedPluginImplementation implements QualifiedPlugin {
+
+ @Override
+ public boolean supports(String delimiter) {
+ return true;
+ }
+ }
+}