Rob Winch opened SPR-13779 and commented
It would be nice to be able to allow Java Configuration to register multiple types of Beans. For example, right now the Spring Security exposes a Java DSL like this:
public void configure(HttpSecurity http) {
http
.formLogin()
}
This single invocation (made by the developer configuring Spring Security) should ideally create numerous Beans (i.e. UsernamePasswordAuthenticationFilter, AuthenticationEntryPoint, etc) and expose them to the Spring ApplicationContext.
The key takeaway is that a developer should be able to interact with a DSL where a single invocation creates multiple Beans.
This is something Juergen Hoeller and I spoke about briefly at SpringOne that I would like to get on the roadmap (hopefully for Spring 5).
Updated
To elaborate on my comment below, I think it would be nice if we could do something like this:
class MyDsl {
private boolean addABean;
private boolean addBBean;
// getters /setters
}
class MyDslXmlParser {
MyDsl parse(Document d) {
return createDls(d);
}
}
class MyDslParser {
public void registerBeans(MyDsl dsl, BeanFactory) {
if(dsl.isAddABean()) {
bf.registerBean(new A());
}
if(dsl.isAddBBean()) {
bf.registerBean(new B());
}
}
}
I Java Config Users could consume this with:
class JavaConfig {
@Bean
public MyDsl myDsl() {
MyDsl myDsl = new MyDsl();
myDsl.setAddABean(true);
return myDsl;
}
}
and MyDslParser.registerBeans
would automatically be invoked with the proper arguments.
In XML Config users could consume this with:
<mydsl:mydsl aBean="true" />
and MyDslParser.registerBeans
would automatically be invoked with the proper arguments.
This would allow the framework to easily support multiple ways of configuring the Beans.
Issue Links: - #19398 Add a functional way to register a bean ("depends on") - #19979 Functional bean dependencies tracking ("depends on") - #17546 New controller for Spring MVC using Lambda - #9271 easier framework support for the creation & injection of a bean by its class - #21497 Support for conditional registration of functional bean definitions - #18463 Provide registerBean variants based on ResolvableType
2 votes, 16 watchers
Comment From: spring-projects-issues
Juergen Hoeller commented
Rob Winch, could you sketch the bean registration logic for a use case such as the above? What would it take to register those underlying beans via the BeanDefinitionRegistry
, i.e. via GenericBeanDefinition
setup and registerBeanDefinition
calls? Ideally, I'd like to provide something more lambda-oriented than that but for a start it'd be good to understand your needs a bit better.
Comment From: spring-projects-issues
Rob Winch commented
Juergen Hoeller Thanks for reaching out.
I have put together a small (very simplified) sample that demonstrates the use case above. Some of the simplifications are:
- There are hard coded values (i.e. the user is hard coded)
- Not a lot of thought went into code organization, naming, etc. This is only meant to demonstrate the goals.
- There are a lot of missing beans from what would normally be present. For example, at this time I do not create the servlet Filter that performs authorization. There are quite a few other Bean Definitions that I do not create.
Ideally, I'd like to provide something more lambda-oriented than that but for a start it'd be good to understand your needs a bit better.
Part of the reason I like the idea of using BeanDefinition
s is because they tend to handle circular references better. This will almost certainly improve the user experience when they are using Spring Security since it tends to cause all sorts of circular references. An example of such:
* Spring Security's method Security is applied to Spring Data Repositories
* Spring Security's method Security needs to use an AuthenticationManager
* The user provides a custom implementation of AuthenticationManager that is backed by Spring Data
Ultimately, I think it would be awesome if somehow I could reuse the Bean Creation logic for both my XML Namespace and Java Configuration. For example, I might first turn the following XML:
<http>
<form-login/>
</http>
into a Java Bean like:
HttpSecurity http = new HttpSecurity();
http
.formLogin();
Then I can run the HttpSecurity
object through the same logic that creates Beans from Java Config DSL (i.e. the code that creates beans from the HttpSecurity
object).
Cheers, Rob
Comment From: spring-projects-issues
Janne Valkealahti commented
I think these issues for getting proper programmatic registration of beans can be boiled down to a very simple missing feature, from JavaConfig returning a list of beans.
At compile time if I don't know how many instances of MyBean class I have, I either have to use ImportBeanDefinitionRegistrar which most of a times is a bit useless as it can only access annotation info and some resources or use BFPP's. So many times I've hoped that I could just return List
Comment From: spring-projects-issues
Rossen Stoyanchev commented
We have these cases in the MVC Java config:
-
ViewResolver
andHandlerExceptionResolver
beans -- either a default set or the set of instances provided by the application through aWebMvcConfigurer
. Currently we use aViewResolverComposite
and aHandlerExceptionResolverComposite
to wrap these sets but it's not ideal with regards to lifecycle methods since we can't be sure if given instances are already beans or not. -
Optional registration of a
HandlerMapping
depending on static resource and view controller registrations via aWebMvcConfigurer
.
Comment From: spring-projects-issues
Sébastien Deleuze commented
See also my functional Spring Boot draft proposal since I think this use case could take advantage of what is discussed here.
Comment From: spring-projects-issues
Juergen Hoeller commented
So is there anything that we need to do for 5.0 still? If yes, could the stakeholders please summarize their current position :-)
Comment From: spring-projects-issues
Rob Winch commented
Juergen Hoeller Thanks for reaching out. I chatted with Sébastien Deleuze I don't think this is really solved from my perspective. He is going to see if he can prototype out the example I have above and get back to me.
Comment From: spring-projects-issues
Sébastien Deleuze commented
We had a discussion with Rob about his use case. Functional bean registration API is very powerful because it allows to register programmatically beans, using if
or for
statements, but maybe the missing point is how to integrate properly in a JavaConfig based Spring application (typically a Spring Boot one).
The most important need I have identified about the feature discussed here is that Spring Framework should provide a way to contribute some beans with the functional bean registration API as part of an application that is using XML or JavaConfig, and I am not sure actually how to do that in order to get it invoked at the right moment of the lifecycle.
To express that differently, the need here is to allow Spring Security and other Spring projects to leverage the powerful/flexible bean registration API for there internals while integrating in Spring Boot application that still leverage JavaConfig for users beans or Spring Boot internals. So it seems to me that we need to have a bridge between JavaConfig and functional bean rehgistration API to use both in the same application. That would be super useful for Spring Boot as well (discussion on this issue shows that there is no easy way to do that currently).
Another important point is how the MyDsl
object will be provided. If we take Spring Securitry example, the DSL is what the user provides using @EnableWebSecurity
+ WebSecurityConfigurerAdapter
overriden methods. If we take a concrete example, currently Spring Security allows to specify its configuration via a Java DSL that leverage internally @Import
to create a few beans + META-INF/spring.factories
to create object instances that are not beans because of the current limitation of JavaConfig:
@Configuration
@EnableWebSecurity
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().hasRole("USER")
.and()
// Possibly more configuration ...
.formLogin() // enable form based log in
// set permitAll for all URLs associated with Form Login
.permitAll();
}
@Bean
public Foo fooBean() { ... }
@Bean
public Bar barBean(Foo fooBean) { ... }
}
The purpose of the feature discussed on this issue would be IMO to provide a way for Spring Security to provide a registerBeansWithFunctionalApi
method that could invoke something like configure(HttpSecurity http)
to allow the user to specify his configuration using the Java DSL, and then to perform various context.registerBean
invocations to register beans consitionnaly based on what the user has register.
For example, it would be nice for Spring Security to be able to do that kind of things:
public class WebSecurityConfigurerAdapter {
@FunctionalBeanRegistration
public void registerBeansWithFunctionalApi(GenericApplicationContext context) {
HttpSecurity http = configure(new HttpSecurity());
HttpDsl httpDsl = http.generateDsl();
if (httpDsl.isAddFooBean()) {
context.registerBean(Foo.class);
if (httpDsl.isAddBarBean()) {
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
}
}
}
}
I am not sure at all there is a need for a dedicated annotation for that, but the idea is to provide an extension point that can allow a JavaConfig Spring application to leverage functional bean registration API. Instantiating ApplicationContext
and calling refresh()
would still be manage by JavaConfig.
Comment From: spring-projects-issues
Janne Valkealahti commented
One of the easiest examples to show what we're missing from a programmatic registration is how Spring Integration javadsl fails. If taking below example which creates gateway and registers it to app context, you can't ever auto-wire it because only hook they have is registerSingleton and that is called only after spring tries to auto-wire beans.
@Bean
public IntegrationFlow iotGatewayFlow() {
return IntegrationFlows
.from(MyGatewayInterface.class)
.get();
}
IntegrationFlowBeanPostProcessor.java#L283
this.beanFactory.registerSingleton(beanName, component);
Comment From: spring-projects-issues
Juergen Hoeller commented
I've done some local tests with straight use of an injected GenericApplicationContext
, and this seems to work fine for me...
@Configuration
public class MyConfigClass {
@Autowired
public void register(GenericApplicationContext ctx) {
ctx.registerBean(...);
}
@Bean
public MyOtherBean() {
....
}
}
Anything I'm missing here?
Comment From: spring-projects-issues
Janne Valkealahti commented
I've never seen any of our own code to directly use GenericApplicationContext, probably for a good reason as I'd assume it opens a can of worms to all sort of other issues which are potentially impossible to track down.
Lets say that there are multiple @Configuration
classes which register their own MyOtherBean's and then some other class injects List
Would it be bad to have some sort of an annotation which would instruct context that this specific method will eventually provide/register beans of certain type? Not sure I like this idea myself either but we do have a chicken/egg situation here and all these are really starting to limit what we can do in all other Spring umbrella projects.
Comment From: spring-projects-issues
Sébastien Deleuze commented
Juergen Hoeller I made a try with my MiXiT application (which is a Spring Boot + Kotlin application), if I replace
@SpringBootApplication
@EnableConfigurationProperties(MixitProperties::class)
class MixitApplication {
@Bean
fun viewResolver(messageSource: MessageSource, properties: MixitProperties) = MustacheViewResolver().apply {
val prefix = "classpath:/templates/"
val suffix = ".mustache"
val loader = MustacheResourceTemplateLoader(prefix, suffix)
setPrefix(prefix)
setSuffix(suffix)
setCompiler(Mustache.compiler().escapeHTML(false).withLoader(loader))
}
@Bean
fun filter(properties: MixitProperties) = MixitWebFilter(properties)
@Bean
fun markdownConverter() = MarkdownConverter()
}
By
@SpringBootApplication
@EnableConfigurationProperties(MixitProperties::class)
class MixitApplication {
@Autowired
fun register(ctx: GenericApplicationContext) {
ctx.registerBean {
MustacheViewResolver().apply {
val prefix = "classpath:/templates/"
val suffix = ".mustache"
val loader = MustacheResourceTemplateLoader(prefix, suffix)
setPrefix(prefix)
setSuffix(suffix)
setCompiler(Mustache.compiler().escapeHTML(false).withLoader(loader))
}
}
ctx.registerBean<MixitWebFilter>()
ctx.registerBean<MarkdownConverter>()
}
}
I get the following error :
Parameter 2 of constructor in mixit.web.handler.BlogHandler required a bean of type 'mixit.util.MarkdownConverter' that could not be found.
Action:
Consider defining a bean of type 'mixit.util.MarkdownConverter' in your configuration.
Comment From: spring-projects-issues
Juergen Hoeller commented
Janne Valkealahti, Sébastien Deleuze, good points: Such @Autowired
-driven registrations work in general but they might come in too late for other injection points. I'll see what we can do about this, probably enforcing such a callback at BeanDefinitionRegistryPostProcessor
time.
Comment From: spring-projects-issues
Sébastien Deleuze commented
Here is a quick update on the Spring Boot + functional bean registration use case : in addition to @Autowired
-driven registration, the other way to register beans with Boot is via using ApplicationContextInitializer
with SpringApplication
API, as described in this comment.
When this issue will be fixed, I will check both works with MiXiT application.
Comment From: spring-projects-issues
Juergen Hoeller commented
I still don't have a clear enough vision of a dedicated first-class mechanism here, so I'd rather defer this to 5.1. The existing mechanisms remain in place: functional registration works in custom BeanDefinitionRegistryPostProcessor
and ApplicationContextInitializer
implementations which can be mixed and matched with configuration classes. There is just no specific callback arrangement for functional registration within configuration classes yet.
Comment From: sdeleuze
I would like to provide an updated POV on that issue based on the use cases we see on Spring Native side and based on latest @jhoeller feedback.
While working on native support, we have seen some consistent patterns emerging and requiring manual native configuration because reflection based. In most cases, using more functional constructs allows native-image
compiler to include automatically the required code via pure static analysis.
The pattern we see is typically for advanced configuration of let say Spring Security or Spring Data where regular @Configuration
are not dynamic enough. I think there are 2 complementary ways to solve that:
- #15920 to provide more flexibility at Framework level (so usable directly by Spring Security or Spring Data) in a declarative fashion
- This issue where we potentially could introduce a dedicated functional bean API that would be used to replace/evolve ImportSelector
(by design very reflection oriented with its List<String>
return type), BeanDefinitionRegistryPostProcessor
and ImportBeanDefinitionRegistrar
(here the functional variant would be conceptually the same than the beanDefinition
based API but exposed with a lambda style that could be seen as a natural Java 8+ based evolution, with better native compatibility).
As pointed out by Juergen, it is currently already theoretically possible by casting BeanDefinitionRegistry
to GenericApplicationContext
. Also:
At the moment, supplier-based registration works everywhere via a
GenericBeanDefinition
andsetInstanceSupplier
, then passed to plainBeanDefinitionRegistry.registerBeanDefinition
So a potential outcome of this issue could be a dedicated functional contract (like the registerBean
methods on GenericApplicationContext
) that could be triggered from configuration classes, in order to provide more guidance (with related documentation) and discoverability to projects like Spring Data, Spring Security or even third party ones.
It could be done via @EnableFoo
annotations and their related imports, and transformed to a more programmatic approach by a build time transformation for native needs. I am not sure yet there is a need to allow that from within configuration class, but to be discussed.
cc @aclement @dsyer @bclozel @rwinch @mp911de @christophstrobl
Comment From: mp911de
Spring Data's repository bean registrations make use of BeanDefinitionBuilder
and BeanDefinitionRegistry.registerBeanDefinition(…)
to register beans. We attempt also to delay class initialization to avoid loading classes unless required as eager class loading may interfere with AOP (specifically EclipseLink) or when using different classloaders.
In terms of reflection, we have few types (e.g. JpaRepositoryFactoryBean
, EntityManagerBeanDefinitionRegistrarPostProcessor
, JpaMetamodelMappingContextFactoryBean
, PersistenceAnnotationBeanPostProcessor
, `repository fragments) that are affected. Since repository fragments require reflection then from a Spring Data perspective we could optimize away.
We use import selectors also for e.g. @EnableJpaAuditing
to obtain the annotation metadata and configure based on the annotation attributes how the beans get instantiated and things like autowireMode
. Auditing is a pretty static arrangement with a static number of beans to register.
Comment From: sdeleuze
We are not sure yet what the outcome of this issue will be, and if there will be an outcome, but we should move forward and close it in Spring Framework 7.0 timeframe, either providing related capabilities or providing guidance for the need that has been described by @rwinch.
Comment From: sdeleuze
I keep seeing a strong need for programmatic bean registration within configuration classes in multiple places of the Spring portfolio (recently in Spring AI). It seems to mainly come from a need for more flexibility in how beans are registered, with 3 popular use-cases:
- Ability to register multiple beans in one @Configuration
method.
- Have more flexibility like conditionally register beans.
- Better integration of the Kotlin bean registration DSL in Spring Boot.
I also would like to provide a follow-up to https://github.com/spring-projects-experimental/spring-fu which had its development stopped a few years ago despite a high potential and a quite a lot of developers excited about it. I would like to do so before moving it to https://github.com/spring-attic/. There was 3 key aspects in Spring Fu: - Efficiency: led to creation of Spring AOT optimizations, allowing GraalVM native image compatibility and JVM optimizations - Functional programming model where we have a subset of Spring Fu via the beans and web functional APIs - Explicit configuration: this one is partially incompatible with Spring Boot core principles, at least currently
That said, something is missing to make functional bean registration a first class citizen in Spring, and I think the Spring Boot issue Expose the functional bean registration API via SpringApplication #8115 opened since 2017 illustrates the need but also the fact that we are not sure how to address it. Also, the main reason why I stopped working on Spring Fu was the lack of consistency with Spring Boot programming model and conventions. We need to unify both annotation and functional configuration worlds and I am not sure we miss a lot of things to succeed.
I have been working on a concrete proposal for this, introducing the concept of functional configuration, with a related draft branch. I have tried to provide an approach that makes sense for both Java and Kotlin.
In Java:
record Foo() {}
record Bar(Foo foo) {}
record Baz(String message) {}
record Boo(String message) {}
@Configuration
class FunctionalConfiguration implements FunctionalInitializer {
@Bean
Baz baz() {
return new Baz("Hello World!");
}
@Override
public void initialize(RegistrableApplicationContext context) {
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
if (context.getEnvironment().matchesProfiles("myProfile")) {
context.registerBean("customName", Baz.class, this::baz);
}
}
}
In Kotlin:
class Foo
class Bar(private val foo: Foo)
class Baz(var message: String = "")
class Boo(var message: String = "")
@Configuration
class FunctionalConfiguration : FunctionalInitializer {
@Bean
fun baz() = Baz("Hello World!")
override fun initialize(context: RegistrableApplicationContext) = beans(context) {
bean<Foo>()
bean<Bar>() // Autowiring of parameters supported by the Kotlin bean DSL
profile("myProfile") {
bean("customName") {
baz()
}
}
}
}
Not 100% sure yet if what I propose here fully makes sense and is able to support all the use cases, there are other options possibles, but the key points here is to allow bridging cleanly the 2 styles of bean declaration we have already, rather than creating a 3rd confusing one. It would allow to discover functional bean registrations naturally in Spring (Boot) applications, allowing a bit of Spring Fu spirit in Spring Boot for those who prefer this kind of programming model, but would also allow Spring portfolio projects which have this need to perform advanced registration logic with this powerful mecanism.
I would like to get feedback both from the Spring team and the Spring developer community before potentially doing more work on it.
Comment From: joshlong
I love this!
It sort of reminds me of the RuntimeHintsRegistrar stuff: a class that lives next to (or on) the types it modifies or interacts with.
So we could have a single configuration class that provides functional definitions and regular definitions and AOT definitions in the same place. Just add the config class to the autoconfig list and everything else works out
It would be nice if there were functional equivalents to conditionals, tho I have no idea how that would look. Just some way to programmatically and simply ask 'has this bean been defined yet'?
Comment From: philwebb
Being able to register multiple beans might also be helpful for the InterfaceClient
work that @OlgaMaciaszek, @rstoyanchev and @dsyer are working on.
If you squint a bit FunctionalInitializer
looks a bit like ImportBeanDefinitionRegistrar
(except there's no importingClassMetadata
). RegistrableApplicationContext
looks like it might also be quite similar to BeanDefinitionRegistry
.
It would be quite nice if a @Bean
method could return the initializer, rather than the configuration class needing to implement an interface. For example:
@Configuration
class FunctionalConfiguration {
@Bean // or perhaps not needed
static FunctionalInitializer fooBar() {
return (context) -> {
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
if (context.getEnvironment().matchesProfiles("myProfile")) {
context.registerBean("customName", Baz.class, this::baz);
}
}
}
}
Comment From: sdeleuze
It would be nice if there were functional equivalents to conditionals, tho I have no idea how that would look. Just some way to programmatically and simply ask 'has this bean been defined yet'?
For conditions, it is programmatic, so you can just write any Java code. We could potentially add some DSL-ish API on Java side similar to what we do on Kotlin side, and unify both. See for example Kotlin profile("myProfile")
versus Java more verbose context.getEnvironment().matchesProfiles("myProfile"))
.
RegistrableApplicationContext
looks like it might also be quite similar toBeanDefinitionRegistry
In my branch, you can see that it is defined as a ConfigurableApplicationContext
+ BeanDefinitionRegistry
+ some methods that were previously only accessible via GenericApplicationContext
class (which in my branch now implements RegistrableApplicationContext
):
public interface RegistrableApplicationContext extends ConfigurableApplicationContext, BeanDefinitionRegistry {
<T> void registerBean(Class<T> beanClass, @Nullable Object... constructorArgs);
<T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Object... constructorArgs);
<T> void registerBean(Class<T> beanClass, BeanDefinitionCustomizer... customizers);
<T> void registerBean(@Nullable String beanName, Class<T> beanClass, BeanDefinitionCustomizer... customizers);
<T> void registerBean(Class<T> beanClass, Supplier<T> supplier, BeanDefinitionCustomizer... customizers);
<T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Supplier<T> supplier, BeanDefinitionCustomizer... customizers);
}
Comment From: dsyer
It’s very much along the lines of what @OlgaMaciaszek and I have been working on. We have some lessons and some wrinkles to iron out. A first class support API would be perfect.
Comment From: sdeleuze
Glad to see some interest.
For methods returning FunctionalInitializer
, that's indeed a reasonable possibility to explore. I played with that but conceptually that's not @Bean
IMO, more like an hypothetical @Beans
, so I in fact a pretty good fit with @Configuration
that can register multiple beans, hence my proposal. We can explore alternatives syntax before deciding.
I am not sure I like static
methods as first class concept in the Java configuration model, but one thing I could like about static
method equivalent in Kotlin would be a top level function like (modified after @dsyer feedback):
@Configuration // does not work for now as it can't be applied to a function
fun myConfiguration(): FunctionalInitializer = beans {
bean<Foo>()
bean<Bar>() // Autowiring of parameters supported by the Kotlin bean DSL
profile("myProfile") {
bean("customName") {
ref<Baz>()
}
}
}
Comment From: wakingrufus
I like the proposal. It would make it a lot easier for app devs to start using the beans dsl without knowing how to write and wire up initializers (or use something like spring-funk, shameless plug). This will promote this approach and make it more popular. As for framework devs, i would much rather see more pure context initalizer libraries, which work even without autoconfiguration, rather than just bolting beans dsls on top of autoconfiguration, but maybe this is a good stepping stone toward that.
Comment From: sdeleuze
Glad you like it. A key point of that proposal is that it would allow libraries to leverage existing @Configuration
discovery mechanism to detect initializers.
It is on purpose not opinionated on explicit versus auto-configuration. That means you could leverage it in spring-funk (your framework inspired from Spring Fu) with a more explicit configuration model, and Spring Boot could continue to use its auto-configuration. Maybe it will allow further evolution, but that's outside of the scope of this issue and of Spring Framework.
Comment From: wakingrufus
It is on purpose not opinionated on explicit versus auto-configuration. That means you could leverage it in spring-funk (your framework inspired from Spring Fu) with a more explicit configuration model, and Spring Boot could continue to use its auto-configuration.
Interesting. I'm still learning about these inner workings. So in this case, would the explicit configuration style use spring.factories or would it use @Import
of the Configuration class? I guess I haven't really considered the idea of using Configuration annotations in the absence of AutoConfiguration. I assume this would use some reflection, but at least avoid the full multi-stage classpath scan?
Comment From: dsyer
@Configuration // does not work for now as it can't be applied to a function fun myConfiguration(baz: Baz): FunctionalInitializer = beans { ... }
You'd have to show it in Java for me to know for sure, but it looks like there's a trap there with the Baz
being injected too early - myConfiguration()
has to be called by framework at bean registration time, before the rest of the context has fully refreshed, and therefore before a @Bean
of type Baz
can expect to be fully initialized. The user could pass in an ObjectFactory<Baz>
, or any other lazy idiom, as long as they don't use it directly in the method body (has to be deferred into a Supplier
or similar). We already hit that snag in Spring gRPC. Maybe framework could do something clever and make the Baz
into a lazy proxy? User would still need to not use it directly, but it would be slightly less awkward.
Comment From: wakingrufus
@Configuration // does not work for now as it can't be applied to a function fun myConfiguration(baz: Baz): FunctionalInitializer = beans { ... }
You'd have to show it in Java for me to know for sure, but it looks like there's a trap there with the
Baz
being injected too early -myConfiguration()
has to be called by framework at bean registration time, before the rest of the context has fully refreshed, and therefore before a@Bean
of typeBaz
can expect to be fully initialized. The user could pass in anObjectFactory<Baz>
, or any other lazy idiom, as long as they don't use it directly in the method body (has to be deferred into aSupplier
or similar). We already hit that snag in Spring gRPC. Maybe framework could do something clever and make theBaz
into a lazy proxy? User would still need to not use it directly, but it would be slightly less awkward.
I solved the injection problem in spring-funk for router dsls via a function like this:
inline fun <reified P1> router(
crossinline f: BeanDefinitionDsl.BeanSupplierContext.(dep1: P1) -> RouterFunctionDsl.() -> Unit
) {
router { org.springframework.web.servlet.function.router { f(ref()).invoke(this) } }
}
I think something similar could be done here. The downside is that it only has single arity so i needed to make one for each arity I wanted to support. But it solves the laziness issue as well as removes the need to declare a separate function. It can all be inlined into the DSL. Here's my code: https://github.com/wakingrufus/spring-funk/blob/main/spring-funk-webmvc/src/main/kotlin/com/github/wakingrufus/funk/webmvc/RoutesDsl.kt
Comment From: sdeleuze
@dsyer This kind of injection of Baz
may indeed not be possible at least initially. I may focus on just @Configuration
+ initializer initially. I will refine based on my progresses on the related branch.
Comment From: snicoll
It would be quite nice if a @Bean method could return the initializer, rather than the configuration class needing to implement an interface.
Unfortunately, I don't think that's a good idea as we're mixing layers. FunctionalInitializer
would then need to be instantiated to prepare the BeanFactory
and that's not really what we're supposed to do. thinking simply about AOT, this would require us to instantiate both the configuration class and the bean itself (both of them could be able to inject whatever which would force us to instantiate those beans at build time).
Implementing the interface will be challenging for the same reason. To me, we need to get away from the configuration class model, or make the method to invoke static to prevent those unwanted bean instantiation. Being able to share conditions and having the programmatic method to be only invoked when the condition matches would be quite interesting as well.
Comment From: sdeleuze
Thanks @snicoll, I will try to take in account those constraints in the next iteration. The main need is discoverability of those functional initializers, we don't have to use @Configuration
infra and programming model in a mandatory way. In fact, I see various reasons for maybe try something slightly different. I will work on a prototype and share the outcome here.
Comment From: OlgaMaciaszek
I second what @dsyer has said about this being potentially really useful for implementing the interface/ grpc clients autoconfiguration features, and also that the ability to pass some kind of instance supplier or other lazy proxy mechanism for beans the configuration would interact with would be important.
Comment From: sdeleuze
I spent a fair amount of time implementing and comparing both approaches (@Configuration
classes implementing an initializer interface versus static methods return type implementing it) and the former is from my point of view better. The implementation is simpler, more efficient, it embraces the fact that conceptually @Configuration
declares a set of beans, it helps to clearly separate functional or @Bean
modes (which echoes to @snicoll concerns that I share), and it allows to support all metadata types in a consistent way.
So I am pleased to share a first draft implementation of the support for functional bean registration in @Configuration
classes including a first set of related tests.
This commit introduces a new RegistrableApplicationContext
interface that exposes GenericApplicationContext
registerBean
methods previously only available in the implementation. This new interface is used in the new RegistrationInitializer
one that extends ApplicationContextInitializer<RegistrableApplicationContext>
in order to provide a more convenient SPI for functional bean registration that is now used in BeanDefinitionDsl
.
@Configuration
classes now support functional bean registration when implementing RegistrationInitializer
which is supported in both Java and Kotlin with GenericApplicationContext
.
It is on purpose not possible to mix functional bean registration and @Bean
in the same @Configuration
class, and @Configuration
classes implementing RegistrationInitializer
are declared with a ROLE_INFRASTRUCTURE
.
Not tested yet, but this should allow to combine class-level annotation-based conditions with programmatic ones in the initialize(...)
implementation and should work with Spring AOT.
Example in Java:
@Configuration
public class FunctionalConfiguration implements RegistrationInitializer {
@Override
public void initialize(RegistrableApplicationContext context) {
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
context.registerBean(Init.class);
if (context.getEnvironment().matchesProfiles("baz")) {
context.registerBean(Baz.class, () -> new Baz("Hello World!"));
}
}
public record Foo() {}
public record Bar(Foo foo) {}
public record Baz(String message) {}
public static class Init implements InitializingBean {
public boolean initialized = false;
@Override
public void afterPropertiesSet() throws Exception {
initialized = true;
}
}
}
Example in Kotlin:
@Configuration
class KotlinFunctionalConfiguration : BeanDefinitionDsl({
bean<Foo>()
bean<Bar>() // Autowiring of parameters supported by the Kotlin bean DSL
profile("baz") {
bean { Baz("Hello World!") }
}
bean<Init>()
})
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
}
}
Comment From: sdeleuze
I am going to significantly refactor this proposal after the brainstorming session I just had with @jhoeller, stay tuned.
Comment From: sdeleuze
Edit: BeanRegistrar
s are now referenced from @Configuration
classes with @Import
.
I have forced-pushed on the same branch the implementation of the design we discussed with @jhoeller.
We now have a BeanRegistrar
SPI that can be implemented by classes intending to perform programmatic and functional bean registration thanks to a BeanRegistry
. Bean registrars can be referenced from @Configuration
classes with @Import
.
The software design is now much cleaner because:
- The name are simple and easier to understand.
- BeanRegistrar
feels like a natural good fit for @Import
.
- The underlying building blocks are BeanDefinitionRegistry
and ListableBeanFactory
instead of GenericApplicationContext
- The API exposed in BeanRegistrar#register
is less error-prone and more focused than previous iterations based on ApplicationContextInitializer
.
- The processing is now done purely in ConfigurationClassPostProcessor
instead of GenericApplicationContext
.
- The Java variant now provides a pretty nice DevXP, closer to the Kotlin one.
The BeanRegistrar
interface is defined as:
public interface BeanRegistrar {
void register(BeanRegistry registry, Environment env);
}
Example in Java:
@Configuration
@Import(MyBeanRegistrar.class)
public class MyConfiguration {
}
public class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(Foo.class);
registry.registerBean(Bar.class,
customizer -> {
customizer.setLazyInit(true);
customizer.setScope(Scope.PROTOTYPE);
customizer.setDescription("Custom description");
},
context -> new Bar(context.getBean(Foo.class)));
registry.registerBean(Init.class);
if (env.matchesProfiles("baz")) {
registry.registerBean(Baz.class, context -> new Baz("Hello World!"));
}
}
}
public record Foo() {}
public record Bar(Foo foo) {}
public record Baz(String message) {}
public static class Init implements InitializingBean {
public boolean initialized = false;
@Override
public void afterPropertiesSet() throws Exception {
initialized = true;
}
}
Example in Kotlin (the former BeanDefintionDsl
is deprecated in favor of BeanRegistrarDsl
):
@Configuration(proxyBeanMethods = false)
@Import(MyBeanRegistrar::class)
class MyConfiguration
class MyBeanRegistrar : BeanRegistrarDsl({
registerBean<Foo>()
registerBean<Bar>(
lazyInit = true,
scope = PROTOTYPE,
description = "Custom description") {
Bar(bean<Foo>())
}
if (env.matchesProfiles("baz")) {
registerBean { Baz("Hello World!") }
}
registerBean<Init>()
})
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
}
}
My current plan is to tentatively target Spring Framework 7.0 M3 to ship a first version of this, with additional documentation, test and Spring AOT support.
Comment From: dsyer
I tried it and found that there was no way to set attributes on the registered bean definitions (with a BDRPP
and the GenericApplicationContext
you can add a bean definition postprocessor). Can we add that somehow? Or at least another way to make the functional bean definitions not fail on AOT processing?
Comment From: wakingrufus
This is looking great @sdeleuze !
A couple questions:
Is there a significant performance difference between using an ApplicationContextInitializer with spring.factories vs an Application class (no autoconfiguration) which @Import
s a Configuration class which implements this new interface?
Also, is there a plan to allow IDEs to detect programmatically registered beans? I know spring generates an environment properties metadata file. Is there something similar for beans?
Comment From: anbusampath
BeanRegistrar is nice approach. Do we need to keep below the code block always?
public static class Init implements InitializingBean {
public boolean initialized = false;
@Override
public void afterPropertiesSet() throws Exception {
initialized = true;
}
}
BeanRegistrar
s are now referenced from@Configuration
classes with@Import
.**BeanRegistrar
feels like a natural good fit for@Import
is it additional to previously suggested @Configuration which implements BeanRegistrar?
Comment From: sdeleuze
@dsyer For attributes, I think the design principles of BeanRegistar
would favor exposing higher level features rather han directly exposing attributes. Order support is a good example of that. For Spring AOT, I am working on adding such support, should be available shortly. Do you have other needs?
@wakingrufus With https://github.com/spring-projects/spring-framework/issues/34486 and potential additional optimizations we are going to do, should be very close and should provide better Spring AOT / GraalVM native support, so I would recommend to consider using it for Spring Fu-like use cases. For IDEs, hat's a good point, I plan to discuss it with IntelliJ IDEA team and I will be happy to sync with @martinlippert for Eclipse / VS code.
@anbusampath It is not additional to the previously suggested @Configuration
which implements BeanRegistrar
, it supersedes it. Not sure to understand your question about "Do we need to keep below the code block always", this is just an example of a bean implementing a "special Spring interface".
Comment From: anbusampath
Not sure to understand your question about "Do we need to keep below the code block always", this is just an example of a bean implemented a "special Spring interface".
I meant InitializingBean is mandatory to make bean registration work with BeanRegistrar. Now I got it, it is additional bean implementation(optional).
Comment From: dsyer
BeanRegistar would favor exposing higher level features rather than directly exposing attributes
I can see why you might want to do that, but I still want a BeanDefinitionCustomizer
.
Comment From: sdeleuze
Yes, I am on it, I should not have mixed the bean customizer and the instance supplier. I will fix it shortly.
Comment From: sdeleuze
I just pushed the related modifications and updated the related comment above. Will add more tests to check the behavior before end of the week.
Comment From: wakingrufus
Is the plan to have BeanRegistrarDsl
be source-compatible with BeanDefintionDsl
? or will there have to be a migration to switch?
Comment From: sdeleuze
Migration will be required (but easy, both are pretty close).
Comment From: wakingrufus
Migration will be required (but easy, both are pretty close).
Ok thanks. I'd be happy to help with an openrewrite recipe when the time comes.
Comment From: sdeleuze
After a lot of refinements and polish (thanks @jhoeller for your feedback), I have merged this feature that I consider as a first-class support for programmatic bean registration, including with support for Spring AOT optimizations and GraalVM native images. I will likely refine the AOT support in M4 to support more advanced use cases.
See the related reference documentation for more details.
@rwinch As discussed, your original use case of conditional bean registration depending on another bean/DSL is not yet supported, but let's explore what we can do to help you for this use case, I suggest we continue this exploration as part of https://github.com/spring-projects/spring-framework/issues/21497.
Comment From: wakingrufus
🎉 so happy to see this done! I included a slide showing usage of this new API in my talk yesterday at DevNexus.
Comment From: martinlippert
@wakingrufus With #34486 and potential additional optimizations we are going to do, should be very close and should provide better Spring AOT / GraalVM native support, so I would recommend to consider using it for Spring Fu-like use cases. For IDEs, hat's a good point, I plan to discuss it with IntelliJ IDEA team and I will be happy to sync with @martinlippert for Eclipse / VS code.
Support for this in the Spring Tools
for VSCode and Eclipse is underway via https://github.com/spring-projects/sts4/issues/1498 and https://github.com/spring-projects/sts4/issues/1499.
Comment From: OlgaMaciaszek
This looks great @sdeleuze. From interface clients autoConfiguration perspective, the ability to also add bean aliases, as currently supported through BeanDefinitionHolder
would also be important. Could adding this be considered?
Comment From: sdeleuze
Glad you like it. Should be possible, could you please create a related issue? I will add this in M4.
Comment From: OlgaMaciaszek
@sdeleuze here it is: https://github.com/spring-projects/spring-framework/issues/34599