diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java new file mode 100644 index 0000000000..cc05c62c80 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2025 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.beans.factory; + +import org.springframework.core.env.Environment; + +/** + * Contract for registering beans programmatically. + * + *

Typically imported with an {@link org.springframework.context.annotation.Import @Import} + * annotation on {@link org.springframework.context.annotation.Configuration @Configuration} + * classes. + *

+ * @Configuration
+ * @Import(MyBeanRegistrar.class)
+ * class MyConfiguration {
+ * }
+ * + *

The bean registrar implementation uses {@link BeanRegistry} and {@link Environment} + * APIs to register beans programmatically in a concise and flexible way. + *

+ * class MyBeanRegistrar implements BeanRegistrar {
+ *
+ *     @Override
+ *     public void register(BeanRegistry registry, Environment env) {
+ *         registry.registerBean("foo", Foo.class);
+ *         registry.registerBean("bar", Bar.class, spec -> spec
+ *                 .prototype()
+ *                 .lazyInit()
+ *                 .description("Custom description")
+ *                 .supplier(context -> new Bar(context.bean(Foo.class))));
+ *         if (env.matchesProfiles("baz")) {
+ *             registry.registerBean(Baz.class, spec -> spec
+ *                     .supplier(context -> new Baz("Hello World!")));
+ *         }
+ *     }
+ * }
+ * + *

In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of + * implementing {@code BeanRegistrar}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@FunctionalInterface +public interface BeanRegistrar { + + /** + * Register beans in a programmatic way. + * @param registry the bean registry + * @param env the environment that can be used to get the active profile or some properties + */ + void register(BeanRegistry registry, Environment env); +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java new file mode 100644 index 0000000000..26cbc577ac --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2025 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.beans.factory; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; + +/** + * Used in {@link BeanRegistrar#register(BeanRegistry, Environment)} to expose + * programmatic bean registration capabilities. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public interface BeanRegistry { + + /** + * Register a bean from the given bean class, which will be instantiated + * using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} + * if any. + * @param beanClass the class of the bean + * @return the generated bean name + */ + String registerBean(Class beanClass); + + /** + * Register a bean from the given bean class, customizing it with the customizer + * callback. The bean will be instantiated using the supplier that can be + * configured in the customizer callback, or will be tentatively instantiated + * with its {@link BeanUtils#getResolvableConstructor resolvable constructor} + * otherwise. + * @param beanClass the class of the bean + * @param customizer callback to customize other bean properties than the name + * @return the generated bean name + */ + String registerBean(Class beanClass, Consumer> customizer); + + /** + * Register a bean from the given bean class, which will be instantiated + * using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} + * if any. + * @param name the name of the bean + * @param beanClass the class of the bean + */ + void registerBean(String name, Class beanClass); + + /** + * Register a bean from the given bean class, customizing it with the customizer + * callback. The bean will be instantiated using the supplier that can be + * configured in the customizer callback, or will be tentatively instantiated with its + * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. + * @param name the name of the bean + * @param beanClass the class of the bean + * @param customizer callback to customize other bean properties than the name + */ + void registerBean(String name, Class beanClass, Consumer> customizer); + + /** + * Specification for customizing a bean. + * @param the bean type + */ + interface Spec { + + /** + * Allow for instantiating this bean on a background thread. + * @see AbstractBeanDefinition#setBackgroundInit(boolean) + */ + Spec backgroundInit(); + + /** + * Set a human-readable description of this bean. + * @see BeanDefinition#setDescription(String) + */ + Spec description(String description); + + /** + * Configure this bean as a fallback autowire candidate. + * @see BeanDefinition#setFallback(boolean) + * @see #primary + */ + Spec fallback(); + + /** + * Hint that this bean has an infrastructure role, meaning it has no + * relevance to the end-user. + * @see BeanDefinition#setRole(int) + * @see BeanDefinition#ROLE_INFRASTRUCTURE + */ + Spec infrastructure(); + + /** + * Configure this bean as lazily initialized. + * @see BeanDefinition#setLazyInit(boolean) + */ + Spec lazyInit(); + + /** + * Configure this bean as not a candidate for getting autowired into some + * other bean. + * @see BeanDefinition#setAutowireCandidate(boolean) + */ + Spec notAutowirable(); + + /** + * The sort order of this bean. This is analogous to the + * {@code @Order} annotation. + * @see AbstractBeanDefinition#ORDER_ATTRIBUTE + */ + Spec order(int order); + + /** + * Configure this bean as a primary autowire candidate. + * @see BeanDefinition#setPrimary(boolean) + * @see #fallback + */ + Spec primary(); + + /** + * Configure this bean with a prototype scope. + * @see BeanDefinition#setScope(String) + * @see BeanDefinition#SCOPE_PROTOTYPE + */ + Spec prototype(); + + /** + * Set the supplier to construct a bean instance. + * @see AbstractBeanDefinition#setInstanceSupplier(Supplier) + */ + Spec supplier(Function supplier); + } + + /** + * Context available from the bean instance supplier designed to give access + * to bean dependencies. + */ + interface SupplierContext { + + /** + * Return the bean instance that uniquely matches the given object type, + * if any. + * @param requiredType type the bean must match; can be an interface or + * superclass + * @return an instance of the single bean matching the required type + * @see BeanFactory#getBean(String) + */ + T bean(Class requiredType) throws BeansException; + + /** + * Return an instance, which may be shared or independent, of the + * specified bean. + * @param name the name of the bean to retrieve + * @param requiredType type the bean must match; can be an interface or superclass + * @return an instance of the bean. + * @see BeanFactory#getBean(String, Class) + */ + T bean(String name, Class requiredType) throws BeansException; + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + *

For matching a generic type, consider {@link #beanProvider(ResolvableType)}. + * @param requiredType type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(Class) + */ + ObjectProvider beanProvider(Class requiredType); + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. This variant allows + * for specifying a generic type to match, similar to reflective injection points + * with generic type declarations in method/constructor parameters. + *

Note that collections of beans are not supported here, in contrast to reflective + * injection points. For programmatically retrieving a list of beans matching a + * specific type, specify the actual bean type as an argument here and subsequently + * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. + *

Also, generics matching is strict here, as per the Java assignment rules. + * For lenient fallback matching with unchecked semantics (similar to the 'unchecked' + * Java compiler warning), consider calling {@link #beanProvider(Class)} with the + * raw type as a second step if no full generic match is + * {@link ObjectProvider#getIfAvailable() available} with this variant. + * @param requiredType type the bean must match; can be a generic type declaration + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(ResolvableType) + */ + ObjectProvider beanProvider(ResolvableType requiredType); + } +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java new file mode 100644 index 0000000000..dfc0c0aac9 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-2025 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.beans.factory.support; + +import java.lang.reflect.Constructor; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.core.ResolvableType; +import org.springframework.util.MultiValueMap; + +/** + * {@link BeanRegistry} implementation that delegates to + * {@link BeanDefinitionRegistry} and {@link ListableBeanFactory}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public class BeanRegistryAdapter implements BeanRegistry { + + private final BeanDefinitionRegistry beanRegistry; + + private final ListableBeanFactory beanFactory; + + private final Class beanRegistrarClass; + + private final @Nullable MultiValueMap customizers; + + + public BeanRegistryAdapter(BeanDefinitionRegistry beanRegistry, ListableBeanFactory beanFactory, + Class beanRegistrarClass) { + this(beanRegistry, beanFactory, beanRegistrarClass, null); + } + + public BeanRegistryAdapter(BeanDefinitionRegistry beanRegistry, ListableBeanFactory beanFactory, + Class beanRegistrarClass, @Nullable MultiValueMap customizers) { + + this.beanRegistry = beanRegistry; + this.beanFactory = beanFactory; + this.beanRegistrarClass = beanRegistrarClass; + this.customizers = customizers; + } + + @Override + public String registerBean(Class beanClass) { + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanClass); + return beanName; + } + + @Override + public String registerBean(Class beanClass, Consumer> customizer) { + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanClass, customizer); + return beanName; + } + + @Override + public void registerBean(String name, Class beanClass) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer customizer : this.customizers.get(name)) { + customizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public void registerBean(String name, Class beanClass, Consumer> spec) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + spec.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer customizer : this.customizers.get(name)) { + customizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + + /** + * {@link RootBeanDefinition} subclass for {@code #registerBean} based + * registrations with constructors resolution match{@link BeanUtils#getResolvableConstructor} + * behavior. It also sets the bean registrar class as the source. + */ + @SuppressWarnings("serial") + private static class BeanRegistrarBeanDefinition extends RootBeanDefinition { + + public BeanRegistrarBeanDefinition(Class beanClass, Class beanRegistrarClass) { + super(beanClass); + this.setSource(beanRegistrarClass); + this.setAttribute("aotProcessingIgnoreRegistration", true); + } + + public BeanRegistrarBeanDefinition(BeanRegistrarBeanDefinition original) { + super(original); + } + + @Override + public Constructor @Nullable [] getPreferredConstructors() { + if (this.getInstanceSupplier() != null) { + return null; + } + try { + return new Constructor[] { BeanUtils.getResolvableConstructor(getBeanClass()) }; + } + catch (IllegalStateException ex) { + return null; + } + } + + @Override + public RootBeanDefinition cloneBeanDefinition() { + return new BeanRegistrarBeanDefinition(this); + } + } + + static class BeanSpecAdapter implements Spec { + + private final RootBeanDefinition beanDefinition; + + private final ListableBeanFactory beanFactory; + + public BeanSpecAdapter(RootBeanDefinition beanDefinition, ListableBeanFactory beanFactory) { + this.beanDefinition = beanDefinition; + this.beanFactory = beanFactory; + } + + @Override + public Spec backgroundInit() { + this.beanDefinition.setBackgroundInit(true); + return this; + } + + @Override + public Spec fallback() { + this.beanDefinition.setFallback(true); + return this; + } + + @Override + public Spec primary() { + this.beanDefinition.setPrimary(true); + return this; + } + + @Override + public Spec description(String description) { + this.beanDefinition.setDescription(description); + return this; + } + + @Override + public Spec infrastructure() { + this.beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + return this; + } + + @Override + public Spec lazyInit() { + this.beanDefinition.setLazyInit(true); + return this; + } + + @Override + public Spec notAutowirable() { + this.beanDefinition.setAutowireCandidate(false); + return this; + } + + @Override + public Spec order(int order) { + this.beanDefinition.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, order); + return this; + } + + @Override + public Spec prototype() { + this.beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + return this; + } + + @Override + public Spec supplier(Function supplier) { + this.beanDefinition.setInstanceSupplier(() -> + supplier.apply(new SupplierContextAdapter(this.beanFactory))); + return this; + } + } + + static class SupplierContextAdapter implements SupplierContext { + + private final ListableBeanFactory beanFactory; + + public SupplierContextAdapter(ListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public T bean(Class requiredType) throws BeansException { + return this.beanFactory.getBean(requiredType); + } + + @Override + public T bean(String name, Class requiredType) throws BeansException { + return this.beanFactory.getBean(name, requiredType); + } + + @Override + public ObjectProvider beanProvider(Class requiredType) { + return this.beanFactory.getBeanProvider(requiredType); + } + + @Override + public ObjectProvider beanProvider(ResolvableType requiredType) { + return this.beanFactory.getBeanProvider(requiredType); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java new file mode 100644 index 0000000000..81e0c3840e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java @@ -0,0 +1,297 @@ +/* + * Copyright 2002-2025 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.beans.factory.support; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanRegistryAdapter}. + * + * @author Sebastien Deleuze + */ +public class BeanRegistryAdapterTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final Environment env = new StandardEnvironment(); + + @Test + void defaultBackgroundInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isBackgroundInit()).isFalse(); + } + + @Test + void enableBackgroundInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, BackgroundInitBeanRegistrar.class); + new BackgroundInitBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isBackgroundInit()).isTrue(); + } + + @Test + void defaultDescription() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getDescription()).isNull(); + } + + @Test + void customDescription() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, CustomDescriptionBeanRegistrar.class); + new CustomDescriptionBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getDescription()).isEqualTo("custom"); + } + + @Test + void defaultFallback() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isFallback()).isFalse(); + } + + @Test + void enableFallback() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, FallbackBeanRegistrar.class); + new FallbackBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isFallback()).isTrue(); + } + + @Test + void defaultRole() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getRole()).isEqualTo(AbstractBeanDefinition.ROLE_APPLICATION); + } + + @Test + void infrastructureRole() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, InfrastructureBeanRegistrar.class); + new InfrastructureBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getRole()).isEqualTo(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); + } + + @Test + void defaultLazyInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isLazyInit()).isFalse(); + } + + @Test + void enableLazyInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, LazyInitBeanRegistrar.class); + new LazyInitBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isLazyInit()).isTrue(); + } + + @Test + void defaultAutowirable() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isAutowireCandidate()).isTrue(); + } + + @Test + void notAutowirable() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, NotAutowirableBeanRegistrar.class); + new NotAutowirableBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isAutowireCandidate()).isFalse(); + } + + @Test + void defaultOrder() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + Integer order = (Integer)beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + assertThat(order).isNull(); + } + + @Test + void customOrder() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, CustomOrderBeanRegistrar.class); + new CustomOrderBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + Integer order = (Integer)beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + assertThat(order).isEqualTo(1); + } + + @Test + void defaultPrimary() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isPrimary()).isFalse(); + } + + @Test + void enablePrimary() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, PrimaryBeanRegistrar.class); + new PrimaryBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isPrimary()).isTrue(); + } + + @Test + void defaultScope() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getScope()).isEqualTo(AbstractBeanDefinition.SCOPE_DEFAULT); + } + + @Test + void prototypeScope() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, PrototypeBeanRegistrar.class); + new PrototypeBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getScope()).isEqualTo(AbstractBeanDefinition.SCOPE_PROTOTYPE); + } + + @Test + void defaultSupplier() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getInstanceSupplier()).isNull(); + } + + @Test + void customSupplier() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, SupplierBeanRegistrar.class); + new SupplierBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)this.beanFactory.getBeanDefinition("foo"); + Supplier supplier = beanDefinition.getInstanceSupplier(); + assertThat(supplier).isNotNull(); + assertThat(supplier.get()).isNotNull().isInstanceOf(Foo.class); + } + + + private static class DefaultBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + } + } + + private static class BackgroundInitBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::backgroundInit); + } + } + + private static class CustomDescriptionBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.description("custom")); + } + } + + private static class FallbackBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::fallback); + } + } + + private static class InfrastructureBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::infrastructure); + } + } + + private static class LazyInitBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::lazyInit); + } + } + + private static class NotAutowirableBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::notAutowirable); + } + } + + private static class CustomOrderBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.order(1)); + } + } + + private static class PrimaryBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::primary); + } + } + + private static class PrototypeBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::prototype); + } + } + + private static class SupplierBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.supplier(context -> new Foo())); + } + } + + private static class Foo {} + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index f9258139e1..da4e23a2c2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -24,6 +24,7 @@ import java.util.Set; import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -65,6 +66,8 @@ final class ConfigurationClass { private final Map> importedResources = new LinkedHashMap<>(); + private final Set beanRegistrars = new LinkedHashSet<>(); + private final Map importBeanDefinitionRegistrars = new LinkedHashMap<>(); @@ -219,6 +222,14 @@ final class ConfigurationClass { return this.importedResources; } + void addBeanRegistrar(BeanRegistrar beanRegistrar) { + this.beanRegistrars.add(beanRegistrar); + } + + public Set getBeanRegistrars() { + return this.beanRegistrars; + } + void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 65f8bc5150..1688542e13 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -29,6 +29,8 @@ import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; @@ -41,6 +43,7 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; @@ -146,7 +149,8 @@ class ConfigurationClassBeanDefinitionReader { } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); - loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromImportBeanDefinitionRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromBeanRegistrars(configClass.getBeanRegistrars()); } /** @@ -395,11 +399,19 @@ class ConfigurationClassBeanDefinitionReader { }); } - private void loadBeanDefinitionsFromRegistrars(Map registrars) { + private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(Map registrars) { registrars.forEach((registrar, metadata) -> registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); } + private void loadBeanDefinitionsFromBeanRegistrars(Set registrars) { + Assert.isInstanceOf(ListableBeanFactory.class, this.registry, + "Cannot support bean registrars since " + this.registry.getClass().getName() + + " does not implement BeanDefinitionRegistry"); + registrars.forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry, + (ListableBeanFactory) this.registry, registrar.getClass()), this.environment)); + } + /** * {@link RootBeanDefinition} marker subclass used to signify that a bean definition diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index f34a326481..54250ee20a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -40,6 +40,7 @@ import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -597,6 +598,13 @@ class ConfigurationClassParser { processImports(configClass, currentSourceClass, importSourceClasses, filter, false); } } + else if (candidate.isAssignable(BeanRegistrar.class)) { + Class candidateClass = candidate.loadClass(); + BeanRegistrar registrar = + ParserStrategyUtils.instantiateClass(candidateClass, BeanRegistrar.class, + this.environment, this.resourceLoader, this.registry); + configClass.addBeanRegistrar(registrar); + } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index cf39cb8038..57f6ee7d6a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -20,7 +20,9 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,8 +42,12 @@ import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; @@ -49,7 +55,10 @@ import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; @@ -60,6 +69,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.aot.InstanceSupplierCodeGenerator; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -73,6 +83,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; @@ -99,6 +110,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.MethodSpec; @@ -106,6 +118,10 @@ import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; /** * {@link BeanFactoryPostProcessor} used for bootstrapping processing of @@ -181,6 +197,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo @SuppressWarnings("NullAway.Init") private List propertySourceDescriptors; + private Set beanRegistrars = new LinkedHashSet<>(); + @Override public int getOrder() { @@ -323,7 +341,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { boolean hasPropertySourceDescriptors = !CollectionUtils.isEmpty(this.propertySourceDescriptors); boolean hasImportRegistry = beanFactory.containsBean(IMPORT_REGISTRY_BEAN_NAME); - if (hasPropertySourceDescriptors || hasImportRegistry) { + boolean hasBeanRegistrars = !this.beanRegistrars.isEmpty(); + if (hasPropertySourceDescriptors || hasImportRegistry || hasBeanRegistrars) { return (generationContext, code) -> { if (hasPropertySourceDescriptors) { new PropertySourcesAotContribution(this.propertySourceDescriptors, this::resolvePropertySourceLocation) @@ -332,6 +351,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo if (hasImportRegistry) { new ImportAwareAotContribution(beanFactory).applyTo(generationContext, code); } + if (hasBeanRegistrars) { + new BeanRegistrarAotContribution(this.beanRegistrars, beanFactory).applyTo(generationContext, code); + } }; } return null; @@ -420,6 +442,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses); + for (ConfigurationClass configClass : configClasses) { + this.beanRegistrars.addAll(configClass.getBeanRegistrars()); + } alreadyParsed.addAll(configClasses); processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); @@ -815,4 +840,182 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } } + private static class BeanRegistrarAotContribution implements BeanFactoryInitializationAotContribution { + + private static final String CUSTOMIZER_MAP_VARIABLE = "customizers"; + + private static final String ENVIRONMENT_VARIABLE = "environment"; + + private final Set beanRegistrars; + + private final ConfigurableListableBeanFactory beanFactory; + + private final AotServices aotProcessors; + + public BeanRegistrarAotContribution(Set beanRegistrars, ConfigurableListableBeanFactory beanFactory) { + this.beanRegistrars = beanRegistrars; + this.beanFactory = beanFactory; + this.aotProcessors = AotServices.factoriesAndBeans(this.beanFactory).load(BeanRegistrationAotProcessor.class); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add( + "applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext)); + beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference()); + } + + private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) { + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + method.addJavadoc("Apply bean registrars."); + method.addModifiers(Modifier.PRIVATE); + method.addParameter(ListableBeanFactory.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE); + method.addParameter(Environment.class, ENVIRONMENT_VARIABLE); + method.addCode(generateCustomizerMap()); + + for (String name : this.beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition(name); + if (beanDefinition.getSource() instanceof Class sourceClass + && BeanRegistrar.class.isAssignableFrom(sourceClass)) { + + for (BeanRegistrationAotProcessor aotProcessor : this.aotProcessors) { + BeanRegistrationAotContribution contribution = + aotProcessor.processAheadOfTime(RegisteredBean.of(this.beanFactory, name)); + if (contribution != null) { + contribution.applyTo(generationContext, + new UnsupportedBeanRegistrationCode(name, aotProcessor.getClass())); + } + } + if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { + if (rootBeanDefinition.getPreferredConstructors() != null) { + for (Constructor constructor : rootBeanDefinition.getPreferredConstructors()) { + reflectionHints.registerConstructor(constructor, ExecutableMode.INVOKE); + } + } + if (!ObjectUtils.isEmpty(rootBeanDefinition.getInitMethodNames())) { + method.addCode(generateInitDestroyMethods(name, rootBeanDefinition, + rootBeanDefinition.getInitMethodNames(), "setInitMethodNames", reflectionHints)); + } + if (!ObjectUtils.isEmpty(rootBeanDefinition.getDestroyMethodNames())) { + method.addCode(generateInitDestroyMethods(name, rootBeanDefinition, + rootBeanDefinition.getDestroyMethodNames(), "setDestroyMethodNames", reflectionHints)); + } + checkUnsupportedFeatures(rootBeanDefinition); + } + } + } + method.addCode(generateRegisterCode()); + } + + private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) { + if (!ObjectUtils.isEmpty(beanDefinition.getFactoryBeanName())) { + throw new UnsupportedOperationException("AOT post processing of the factory bean name is not supported yet with BeanRegistrar"); + } + if (beanDefinition.hasConstructorArgumentValues()) { + throw new UnsupportedOperationException("AOT post processing of argument values is not supported yet with BeanRegistrar"); + } + if (!beanDefinition.getQualifiers().isEmpty()) { + throw new UnsupportedOperationException("AOT post processing of qualifiers is not supported yet with BeanRegistrar"); + } + for (String attributeName : beanDefinition.attributeNames()) { + if (!attributeName.equals(AbstractBeanDefinition.ORDER_ATTRIBUTE) + && !attributeName.equals("aotProcessingIgnoreRegistration")) { + throw new UnsupportedOperationException("AOT post processing of attribute " + attributeName + + " is not supported yet with BeanRegistrar"); + } + } + } + + private CodeBlock generateCustomizerMap() { + Builder code = CodeBlock.builder(); + code.addStatement("$T<$T, $T> $L = new $T<>()", MultiValueMap.class, String.class, BeanDefinitionCustomizer.class, + CUSTOMIZER_MAP_VARIABLE, LinkedMultiValueMap.class); + return code.build(); + } + + private CodeBlock generateRegisterCode() { + Builder code = CodeBlock.builder(); + for (BeanRegistrar beanRegistrar : this.beanRegistrars) { + code.addStatement("new $T().register(new $T(($T)$L, $L, $T.class, $L), $L)", beanRegistrar.getClass(), + BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, + BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, beanRegistrar.getClass(), CUSTOMIZER_MAP_VARIABLE, + ENVIRONMENT_VARIABLE); + } + return code.build(); + } + + private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition, + String[] methodNames, String method, ReflectionHints reflectionHints) { + + Builder code = CodeBlock.builder(); + // For Publisher-based destroy methods + reflectionHints.registerType(TypeReference.of("org.reactivestreams.Publisher")); + Class beanType = ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass()); + Arrays.stream(methodNames).forEach(methodName -> addInitDestroyHint(beanType, methodName, reflectionHints)); + CodeBlock arguments = Arrays.stream(methodNames) + .map(name -> CodeBlock.of("$S", name)) + .collect(CodeBlock.joining(", ")); + + code.addStatement("$L.add($S, $L -> (($T)$L).$L($L))", CUSTOMIZER_MAP_VARIABLE, beanName, "bd", + AbstractBeanDefinition.class, "bd", method, arguments); + return code.build(); + } + + // Inspired from BeanDefinitionPropertiesCodeGenerator#addInitDestroyHint + private static void addInitDestroyHint(Class beanUserClass, String methodName, ReflectionHints reflectionHints) { + Class methodDeclaringClass = beanUserClass; + + // Parse fully-qualified method name if necessary. + int indexOfDot = methodName.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = methodName.substring(0, indexOfDot); + methodName = methodName.substring(indexOfDot + 1); + if (!beanUserClass.getName().equals(className)) { + try { + methodDeclaringClass = ClassUtils.forName(className, beanUserClass.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to load Class [" + className + + "] from ClassLoader [" + beanUserClass.getClassLoader() + "]", ex); + } + } + } + + Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName); + if (method != null) { + reflectionHints.registerMethod(method, ExecutableMode.INVOKE); + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass); + if (!publiclyAccessibleMethod.equals(method)) { + reflectionHints.registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE); + } + } + } + + + static class UnsupportedBeanRegistrationCode implements BeanRegistrationCode { + + private final String message; + + public UnsupportedBeanRegistrationCode(String beanName, Class aotProcessorClass) { + this.message = "Code generation attempted for bean " + beanName + " by the AOT Processor " + + aotProcessorClass + " is not supported with BeanRegistrar yet"; + } + + @Override + public ClassName getClassName() { + throw new UnsupportedOperationException(this.message); + } + + @Override + public GeneratedMethods getMethods() { + throw new UnsupportedOperationException(this.message); + } + + @Override + public void addInstancePostProcessor(MethodReference methodReference) { + throw new UnsupportedOperationException(this.message); + } + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Import.java b/spring-context/src/main/java/org/springframework/context/annotation/Import.java index 9c905d0d0b..c1275801fc 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Import.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Import.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.beans.factory.BeanRegistrar; + /** * Indicates one or more component classes to import — typically * {@link Configuration @Configuration} classes. @@ -57,7 +59,8 @@ public @interface Import { /** * {@link Configuration @Configuration}, {@link ImportSelector}, - * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. + * {@link ImportBeanDefinitionRegistrar}, {@link BeanRegistrar} or regular + * component classes to import. */ Class[] value(); diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java index 6f52e00448..0b6a6fda54 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java @@ -30,6 +30,7 @@ import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -150,6 +151,7 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia private @Nullable CodeBlock apply(ClassName className) { String name = className.canonicalName(); if (name.equals(DefaultListableBeanFactory.class.getName()) + || name.equals(ListableBeanFactory.class.getName()) || name.equals(ConfigurableListableBeanFactory.class.getName())) { return CodeBlock.of(BEAN_FACTORY_VARIABLE); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 7d238cb6a2..089ee600b2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; @@ -24,6 +25,7 @@ import java.util.function.Predicate; import javax.lang.model.element.Modifier; +import jakarta.annotation.PostConstruct; import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; @@ -37,7 +39,10 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -54,6 +59,7 @@ import org.springframework.context.testfixture.context.annotation.SimpleConfigur import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.DefaultPropertySourceFactory; import org.springframework.core.test.tools.Compiled; @@ -74,8 +80,9 @@ import static org.assertj.core.api.Assertions.entry; * @author Phillip Webb * @author Stephane Nicoll * @author Sam Brannen + * @author Sebastien Deleuze */ -class ConfigurationClassPostProcessorAotContributionTests { +public class ConfigurationClassPostProcessorAotContributionTests { private final TestGenerationContext generationContext = new TestGenerationContext(); @@ -439,6 +446,135 @@ class ConfigurationClassPostProcessorAotContributionTests { } } + @Nested + public class BeanRegistrarTests { + + @Test + void applyToWhenHasDefaultConstructor() throws NoSuchMethodException { + BeanFactoryInitializationAotContribution contribution = getContribution(DefaultConstructorConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + Constructor fooConstructor = Foo.class.getDeclaredConstructor(); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor)) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasInstanceSupplier() { + BeanFactoryInitializationAotContribution contribution = getContribution(InstanceSupplierConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(generationContext.getRuntimeHints().reflection().getTypeHint(Foo.class)).isNull(); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasPostConstructAnnotationPostProcessed() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + PostConstructConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + Init init = freshContext.getBean(Init.class); + assertThat(init).isNotNull(); + assertThat(init.initialized).isTrue(); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Init.class, "postConstruct")) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + private void compile(BiConsumer, Compiled> result) { + MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0); + beanFactoryInitializationCode.getTypeBuilder().set(type -> { + ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator + .of(ListableBeanFactory.class, "applicationContext.getBeanFactory()") + .and(ArgumentCodeGenerator.of(Environment.class, "applicationContext.getEnvironment()")); + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(argCodeGenerator, + beanFactoryInitializationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, GenericApplicationContext.class)); + type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC) + .addParameter(GenericApplicationContext.class, "applicationContext") + .addStatement(methodInvocation) + .build()); + }); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(Consumer.class), compiled)); + } + + + @Configuration + @Import(DefaultConstructorBeanRegistrar.class) + public static class DefaultConstructorConfiguration { + } + + public static class DefaultConstructorBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class); + } + } + + @Configuration + @Import(InstanceSupplierBeanRegistrar.class) + public static class InstanceSupplierConfiguration { + } + + public static class InstanceSupplierBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class, spec -> spec.supplier(context -> new Foo())); + } + } + + @Configuration + @Import(PostConstructBeanRegistrar.class) + public static class PostConstructConfiguration { + } + + public static class PostConstructBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Init.class); + } + } + + static class Foo { + } + + static class Init { + + boolean initialized = false; + + @PostConstruct + void postConstruct() { + initialized = true; + } + } + + } + private @Nullable BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java new file mode 100644 index 0000000000..5cbbb4e44c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2025 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.context.annotation.beanregistrar; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Bar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Baz; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init; +import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}. + * + * @author Sebastien Deleuze + */ +public class BeanRegistrarConfigurationTests { + + @Test + void beanRegistrar() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanRegistrarConfiguration.class); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class)).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + BeanDefinition beanDefinition = context.getBeanDefinition("bar"); + assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE); + assertThat(beanDefinition.isLazyInit()).isTrue(); + assertThat(beanDefinition.getDescription()).isEqualTo("Custom description"); + } + + @Test + void beanRegistrarWithProfile() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(BeanRegistrarConfiguration.class); + context.getEnvironment().addActiveProfile("baz"); + context.refresh(); + assertThat(context.getBean(Baz.class).message()).isEqualTo("Hello World!"); + } + + @Test + void scannedFunctionalConfiguration() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.testfixture.context.annotation.registrar"); + context.refresh(); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class).message()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java new file mode 100644 index 0000000000..26115c7d77 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2025 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.context.testfixture.beans.factory; + +import jakarta.annotation.PostConstruct; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.env.Environment; + +public class SampleBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + registry.registerBean("bar", Bar.class, spec -> spec + .prototype() + .lazyInit() + .description("Custom description") + .supplier(context -> new Bar(context.bean(Foo.class)))); + if (env.matchesProfiles("baz")) { + registry.registerBean(Baz.class, spec -> spec + .supplier(context -> new Baz("Hello World!"))); + } + registry.registerBean(Init.class); + } + + public record Foo() {} + public record Bar(Foo foo) {} + public record Baz(String message) {} + + public static class Init { + + public boolean initialized = false; + + @PostConstruct + public void postConstruct() { + initialized = true; + } + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java new file mode 100644 index 0000000000..1360caec7e --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2025 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.context.testfixture.context.annotation.registrar; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar; + +@Configuration +@Import(SampleBeanRegistrar.class) +public class BeanRegistrarConfiguration { +}