Last updated on September 4th, 2023
Spring is one of the most widely used Java EE frameworks. As with any framework, its utility grows exponentially once you understand its inner workings. At its core, Spring Framework is based on two design principles – Dependency Injection and Aspect Oriented Programming. This post is a note in my series which aims to explore the fundamental concepts of the Spring framework. Let’s figure out the Dependency Injection pattern and how it is applied in Spring Framework.
IoC and Dependency injection
IoC – Inversion of Control – is a core pattern applied in the Spring framework. Basically, it is a principle that inverts the control of object creation from our own to an IOC container. We can achieve IoC through various implementations such as Strategy, Factory design pattern, or Dependency Injection pattern. In Spring Framework, we can achieve IoC via Dependency injection implementation.
In Spring, there are three ways of achieving the dependency injection pattern:
- Constructor injection: for mandatory dependencies or when aiming for immutability.
- Setter or other methods of injection for optional or changeable dependencies.
- Through reflection, directly into Field injection.
Constructor injection is considered the best practice in most cases, especially when we aim for immutability. This is because once an object is instantiated, its dependencies cannot be changed. It ensures that all dependencies are available when the bean is created, preventing potential null pointer exceptions. In contrast, Setter and Field injection allow for the modification of dependencies after the object is instantiated, which can make the object’s state unpredictable and harder to maintain. With field injection, dependency problems may not occur until runtime, leading to late error discovery.
The advantages of this architecture are:
- Decoupling: Dependency injection promotes loose coupling between components by allowing the dependencies of a class to be provided by a container, rather than the class itself instantiating the dependent objects. This approach makes it easier to switch between different implementations or modify the dependencies without changing the class code.
- Separation of concerns: components should focus only on the core business logic, and not on creating or managing its dependencies.
- Testability: dependencies being mocked easily, can test individual components in isolation.
How Spring initializes beans
The interface ApplicationContext represents the IoC container in Spring. The Spring container is responsible for managing the application components by creating objects, wiring them together along with configuring and managing their overall life cycle.
This is a high-level overview of how Spring wires beans under the hood:
Basically, Spring implements IoC by using Java Reflection API, proxies, and a variety of other techniques to create, wire, and manage the beans’ lifecycle.
- Bean scanning: When the Spring application starts, it scans the classes and configuration files to create bean definitions. A BeanDefinition contains metadata about the bean class, such as constructor, initialization methods, dependencies, and scope. These
BeanDefinition
objects are internally registered byBeanDefinitionRegistry
which is used by the IoC container later to instantiate and manage the beans.
- Bean instantiation: Then, using Reflection API, Spring instantiates the bean by calling the constructor or the factory method specified in the
BeanDefinition
. Note: before bean instantiation,BeanFactoryPostProcessors
will be called. It can modify or transformBeanDefinition
prior to instantiation. A most familiar exampleBeanFactoryPostProcessors
isPropertySourcesPlaceholderConfigurer
to load properties files at one point.
- Bean dependencies: Once the bean is instantiated, Spring populates its dependencies by either setter injection, field injection, or constructor injection. It uses the Reflection API to set values, invoke setter methods, or pass them as constructor arguments. If the dependencies are defined in XML configuration, Spring converts them from their string representation to their required target types by using PropertyEditors or Converters.
- Bean post-processing: Then, Spring applies registered
BeanPostProcessors
to perform additional customizing or manipulating the bean. For example, proxies for AOP, transaction management, and caching are being created and applied during this stage.
- Bean initialization and destruction: Spring calls
@PostConstruct
,InitializingBean
interfaces or custom init-method after all the properties and dependencies have been set. Once the bean is no longer required and the context is being closed, Spring calls the appropriate destruction callback such as@PreDestroy
,DisposableBean
interfaces or custom destroy-method.
Next steps
We’ve covered the core of Spring Framework. To me, grasping the underlying theory before starting code is extremely important. To further expand your knowledge, you can explore the AOP concept in this article and stay tuned for my upcoming post on Spring design patterns. However, true learning often comes from hands-on experience, so be sure to put theory into practice as you continue your journey with the Spring Framework.