Monday, May 28, 2018

Spring Beans Discovery through Java Config in Practice

Spring's Java Config is great tool giving us precise way to control bootstrap and the wiring of an application. However it does not always work as expected and we are running sometimes into some strange issues, where particular bean it not being instantiated at expected time, or lazy loading is not working as expected, or whole configuration is being ignored for no reason.
So I've decided to go over some examples, to get better understanding of how things are in practice.

Examples can be found here: https://github.com/maciejmiklas/spring-context-test.git
Each example has name, like: exa (example A) or exb (example B). Within each example there are several exercises, like: exa has: exa01, exa02, ..., ex05.

Bean Loading Order - Single Config

exa01

This is the first exercise, it defines basic structure that will be modify in following chapters.
The main method loads spring context from configuration file Conf.java. This configuration file creates 3 beans. Each of those beans has two log statements, first one in constructor, second in @PostConstruct. We can see also the console output after execution of this example.
Nothing spectacular happens here: beans are initialised from A to C, right after constructor Spring executes also @PostConstruct - there are no dependencies between beans.


exa02

Lets modify order of factory methods in Conf.java

Bean initialisation order has changed. This means that method names do not matter, but the physical order within class. I can imagine, that this can change with JVM, so you definitely should not rely on it!


exa03

Now we will extract definition of BeanB into dedicated configuration file:
Spring loads bean definitions from imported configuration in the first place.


exa04

We will modify previous exercise, so that BeanB depends on BeanA. Spring loads dependent bean in the first place, it does not matter where it's located, Spring will scan all possible configurations to locate it.

exa05

The same as exa04, but BeanB does not have @DependsOn("beanA"), instead BeanB injects BeanA. We have logically similar dependency situation to exa4: BanB depends on BeanA. However @PostConstructs are called in different order: Spring creates instance of BeanB, during construction phase (constructor) references to BeanA are not set. Once the instance of BeanB is created, BeanA will be created and injected to BeanB. BeanB cannot access BeanA in constructor, first after full initialisation is done - in method @PostConstruct.
Spring had to modify order of @PostConstruct calls, to ensure that bean references are not null during initialisation phase.


Summary

  • within a single configuration class the bean instantiation order depends only on methods order within this class and not method names. However this might depend on JVM,
  • Spring loads in the first place imported bean definitions,
  • previous rule applies only to situation where direct bean definitions do not have dependencies on imported beans, if this is the case Spring will load dependent bean in the first place,
  • initialisation code has to be placed in @PostConstruct method and not in a constructor, otherwise references to some beans might be null.

Bean Loading Order - Mixed Config


exb01

BeanA, BeanB and BeanC are created through dedicated configuration classes: ConfBeanA, ConfBeanB and ConfBeanC. BeanD is declared in Conf Beans are created in order: A, B, C, D.


exb02

Similar to exb01, but configuration class ConfBeanB has been renamed to ConfBeanXB
The instantiation order remains unchanged.


exb03

The same as exb01 but factory method in ConfBeanB has been renamed from beanB to xyz. The instantiation order remains unchanged.


exb04

The same as exb01 but BeanB has been renamed to BeanXB. This change did not influence instantiation order.


exb05

Similar to exb01 but we've changed import order from A, B, C into B, A, C. The instantiation order has changed as well.


Summary


  • when @Import statement contains multiple classes, Spring will load them from the left to the right side - it's iterating over array, names of configuration classes do not matter in this case,
  • within single configuration class physical order of methods matters, not their names.

Duplicated Bean Name

exc01

We have here 3 beans A, B, C with dedicated config.  BeanA injects BeanB and BeanC.


exc02

The same as exc01, but we've renamed factory method ConfBeanB#beanB() into ConfBeanB#beanC() In order to find out all possible bean definitions Spring goes over our @Import declaration, which is: ConfBeanA, ConfBeanB, ConfBeanC. There are two factory methods with the same name: ConfBeanB#beanC() and ConfBeanC#beanC(), so ConfBeanC overwrites bean created through ConfBeanB, because it creates bean with the same name.


exc03

Similar to exc02 but this time we've renamed method ConfBeanC#beanC() into ConfBeanC#beanB(), so that we have two methods beanB and not as it was in exc02 two methods with name: beanC The output is still the same, BeanB is missing. We've just changed name of factory method, and we already know that names do not change loading order, and we have still two beans with the same name.


exc04

Similar to exc02, but we've changed import order for config classes from A, B, C to A, C, B. Now ConfBeanB will get scanned on the end and it overwrites previous bean with the same name, so BeanC is missing.


exc05 

It's a modified exc02. We've set AllowBeanDefinitionOverriding to false on Application Context. Bean overwriting is disabled now, so instead of bean not found we are getting exception that we are trying to register two bean under the same name.


exc06

The same as exc02, there are still two factory methods: beanC. Additionally BeanB and BeanC are implementing common interface. Now BeanA does not inject directly BeanB and BeanC, but it injects collection of beans implementing our interface. In case of direct injection one of the beans was missing, now both are there!

Summary
In case of two beans with the same name the last one wins. You will not be able to directly inject such overwritten bean, but you can inject collection of such beans sharing common interface.

Lazy Loading


exd01

We have here three beans A, B, C, there are no dependencies between them. BeanC is being defined through dedicated configuration class. Nothing special here, initialisation order is as expected.


exd02

The same as exd02, but we've annotated BeanB as lazy. Spring does not load BeanB at all, there are no dependencies to that bean.


exd03

Now we've injected BeanB into BeanA. @Lazy is set on injection point and bean definition. Additionally we have defined method on BeanA that calls method on BeanB: ApplicationExD03 -> beanA.method() -> beanB.method() Spring postpones creation of BeanB until method is being called on it, @Lazy works as expected.


exd04

Same as exd03, but @Lazy has been removed on injection point. BeanB is not lazy any more, @Lazy on bean definition is not enough.


exd05

Same as exd03, but @Lazy has been removed from bean configuration. BeanB is not lazy any more, @Lazy on bean definition is not enough.


Summary

  • lazy annotation has to be provided on configuration and all injection points, otherwise spring eagerly initialises such bean,
  • lazy beans that are not referenced (injected) are not loaded at all,
  • you should not rely on @Lazy, unless you are 100% sure that you can control all possible injection points, otherwise one missing Lazy-Annotation will disable laziness on such bean.

No comments:

Post a Comment