diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt new file mode 100644 index 00000000000..925e65981fa --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -0,0 +1,383 @@ +/* + * 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.beans.factory.BeanRegistry.SupplierContext +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.ResolvableType +import org.springframework.core.env.Environment + +/** + * Contract for registering programmatically beans. + * + * Typically imported with an `@Import` annotation on `@Configuration` classes. + * ``` + * @Configuration + * @Import(MyBeanRegistrar::class) + * class MyConfiguration { + * } + * ``` + * + * In Kotlin, a bean registrar is typically created with a `BeanRegistrarDsl` to register + * beans programmatically in a concise and flexible way. + * ``` + * class MyBeanRegistrar : BeanRegistrarDsl({ + * registerBean() + * registerBean( + * name = "bar", + * prototype = true, + * lazyInit = true, + * description = "Custom description") { + * Bar(bean()) + * } + * profile("baz") { + * registerBean { Baz("Hello World!") } + * } + * }) + * ``` + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@BeanRegistrarDslMarker +open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): BeanRegistrar { + + @PublishedApi + internal lateinit var registry: BeanRegistry + + /** + * The environment that can be used to get the active profile or some properties. + */ + lateinit var env: Environment + + /** + * Register a bean from the given bean class, which will be instantiated + * using the related [resolvable constructor] + * [org.springframework.beans.BeanUtils.getResolvableConstructor] if any. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + */ + inline fun registerBean(name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + } + registry.registerBean(name, T::class.java, customizer) + } + + /** + * Register a bean from the given bean class, which will be instantiated + * using the related [resolvable constructor] + * [org.springframework.beans.BeanUtils.getResolvableConstructor] + * if any. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @return the generated bean name + */ + inline fun registerBean(autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false): String { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + } + return registry.registerBean(T::class.java, customizer) + } + + /** + * Register a bean from the given bean class, which will be instantiated + * using the provided [supplier]. + * @param T the bean type + * @param name the name of the bean + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @param supplier the supplier to construct a bean instance + */ + inline fun registerBean(name: String, + autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false, + crossinline supplier: (SupplierContextDsl.() -> T)) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it).supplier() + } + } + registry.registerBean(name, T::class.java, customizer) + } + + inline fun registerBean(autowirable: Boolean = true, + backgroundInit: Boolean = false, + description: String? = null, + fallback: Boolean = false, + infrastructure: Boolean = false, + lazyInit: Boolean = false, + order: Int? = null, + primary: Boolean = false, + prototype: Boolean = false, + crossinline supplier: (SupplierContextDsl.() -> T)): String { + /** + * Register a bean from the given bean class, which will be instantiated + * using the provided [supplier]. + * @param T the bean type + * @param autowirable set whether this bean is a candidate for getting + * autowired into some other bean + * @param backgroundInit set whether this bean allows for instantiation + * on a background thread + * @param description a human-readable description of this bean + * @param fallback set whether this bean is a fallback autowire candidate + * @param infrastructure set whether this bean has an infrastructure role, + * meaning it has no relevance to the end-user + * @param lazyInit set whether this bean is lazily initialized + * @param order the sort order of this bean + * @param primary set whether this bean is a primary autowire candidate + * @param prototype set whether this bean has a prototype scope + * @param supplier the supplier to construct a bean instance + */ + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (infrastructure) { + it.infrastructure() + } + if (fallback) { + it.fallback() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it).supplier() + } + } + return registry.registerBean(T::class.java, customizer) + } + + /** + * Apply the nested block if the given profile expression matches the + * active profiles. + * + * A profile expression may contain a simple profile name (for example + * `"production"`) or a compound expression. A compound expression allows + * for more complicated profile logic to be expressed, for example + * `"production & cloud"`. + * + * The following operators are supported in profile expressions: + * - `!` - A logical *NOT* of the profile name or compound expression + * - `&` - A logical *AND* of the profile names or compound expressions + * - `|` - A logical *OR* of the profile names or compound expressions + * + * Please note that the `&` and `|` operators may not be mixed + * without using parentheses. For example, `"a & b | c"` is not a valid + * expression: it must be expressed as `"(a & b) | c"` or `"a & (b | c)"`. + * @param expression the profile expressions to evaluate + */ + fun profile(expression: String, init: BeanRegistrarDsl.() -> Unit) { + if (env.matchesProfiles(expression)) { + init() + } + } + + /** + * Context available from the bean instance supplier designed to give access + * to bean dependencies. + */ + @BeanRegistrarDslMarker + open class SupplierContextDsl(@PublishedApi internal val context: SupplierContext) { + + /** + * Return the bean instance that uniquely matches the given object type, + * and potentially the name if provided, if any. + * @param T the bean type + * @param name the name of the bean + */ + inline fun bean(name: String? = null) : T = when (name) { + null -> beanProvider().getObject() + else -> context.bean(name, T::class.java) + } + + /** + * Return a provider for the specified bean, allowing for lazy on-demand + * retrieval of instances, including availability and uniqueness options. + * @param T type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + */ + inline fun beanProvider() : ObjectProvider = + context.beanProvider(ResolvableType.forType((object : ParameterizedTypeReference() {}).type)) + } + + override fun register(registry: BeanRegistry, env: Environment) { + this.registry = registry + this.env = env + init() + } + +} + +@DslMarker +internal annotation class BeanRegistrarDslMarker diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt index 89c607a6004..af73c5d7ba9 100644 --- a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt +++ b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt @@ -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. @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") package org.springframework.context.support import org.springframework.aot.AotDetector @@ -25,6 +26,8 @@ import org.springframework.beans.factory.getBeanProvider import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.beans.factory.support.BeanDefinitionReaderUtils import org.springframework.context.ApplicationContextInitializer +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.ResolvableType import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.Profiles import java.util.function.Supplier @@ -68,6 +71,7 @@ import java.util.function.Supplier * @see BeanDefinitionDsl * @since 5.0 */ +@Deprecated(message = "Use BeanRegistrarDsl instead") fun beans(init: BeanDefinitionDsl.() -> Unit) = BeanDefinitionDsl(init) /** @@ -79,6 +83,9 @@ fun beans(init: BeanDefinitionDsl.() -> Unit) = BeanDefinitionDsl(init) * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated( + replaceWith = ReplaceWith("BeanRegistrarDsl", "org.springframework.beans.factory.BeanRegistrarDsl"), + message = "Use BeanRegistrarDsl instead") open class BeanDefinitionDsl internal constructor (private val init: BeanDefinitionDsl.() -> Unit, private val condition: (ConfigurableEnvironment) -> Boolean = { true }) : ApplicationContextInitializer { @@ -102,6 +109,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit /** * Scope enum constants. */ + @Deprecated(message = "Use BeanRegistrarDsl instead") enum class Scope { /** @@ -120,6 +128,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit /** * Role enum constants. */ + @Deprecated(message = "Use BeanRegistrarDsl instead") enum class Role { /** diff --git a/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt b/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt new file mode 100644 index 00000000000..6f656b4eec2 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt @@ -0,0 +1,89 @@ +/* + * 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 + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.api.ThrowableAssert +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.getBean +import org.springframework.beans.factory.BeanRegistrarDsl + +/** + * Kotlin tests leveraging [BeanRegistrarDsl]. + * + * @author Sebastien Deleuze + */ +class BeanRegistrarDslConfigurationTests { + + @Test + fun beanRegistrar() { + val context = AnnotationConfigApplicationContext(BeanRegistrarKotlinConfiguration::class.java) + assertThat(context.getBean().foo).isEqualTo(context.getBean()) + assertThatThrownBy(ThrowableAssert.ThrowingCallable { context.getBean() }).isInstanceOf(NoSuchBeanDefinitionException::class.java) + assertThat(context.getBean().initialized).isTrue() + val beanDefinition = context.getBeanDefinition("bar") + assertThat(beanDefinition.scope).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE) + assertThat(beanDefinition.isLazyInit).isTrue() + assertThat(beanDefinition.description).isEqualTo("Custom description") + } + + @Test + fun beanRegistrarWithProfile() { + val context = AnnotationConfigApplicationContext() + context.register(BeanRegistrarKotlinConfiguration::class.java) + context.getEnvironment().addActiveProfile("baz") + context.refresh() + assertThat(context.getBean().foo).isEqualTo(context.getBean()) + assertThat(context.getBean().message).isEqualTo("Hello World!") + assertThat(context.getBean().initialized).isTrue() + } + + class Foo + data class Bar(val foo: Foo) + data class Baz(val message: String = "") + class Init : InitializingBean { + var initialized: Boolean = false + + override fun afterPropertiesSet() { + initialized = true + } + + } + + @Configuration + @Import(SampleBeanRegistrar::class) + internal class BeanRegistrarKotlinConfiguration + + private class SampleBeanRegistrar : BeanRegistrarDsl({ + registerBean() + registerBean( + name = "bar", + prototype = true, + lazyInit = true, + description = "Custom description") { + Bar(bean()) + } + profile("baz") { + registerBean { Baz("Hello World!") } + } + registerBean() + }) +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt index 11bb65c8a91..f4770b5f6c2 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") package org.springframework.context.support import org.assertj.core.api.Assertions.assertThat