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; + } + } +}