Inversion of control in Spring
Spring Boot is the most popular framework for building cloud-native microservices in the Java space. To really understand how any Spring project works, though, you must first understand the IoC design principle and how the Spring framework implements it.
What is inversion of control?
The term inversion of control sounds intimidating, but it's actually a simple concept.
Inversion of control simply asserts that it's best to keep your application's business logic clean and concise. To ensure this, an external, specialized framework handles tasks such as the following:
- Parameterize heavily configurable objects.
- Manage components with complex lifecycle requirements.
- Interact with low-level system resources.
Along with more lean and readable business logic, other benefits of using an inversion of control framework include the following:
- Loose coupling between components.
- Simplified code reusability.
- Enhanced testability.
- Standardized development practices.
If you've ever written a utility class that configures database connections or you've created a component that provides universal access to a property or a service, you've independently entered the world of inversion of control. Popular frameworks such as Spring simply standardize, codify and extend the concept.
Spring IoC example
Let's take a look at a simple example of inversion of control with Spring.
If you've read my article on how to write a Spring Hello World program, you should be comfortable with the following code. If not, take a look at that article to familiarize yourself with the basics of Spring. It's a quick read.
A simple Java class
In this Spring IoC example, we'll work with a class named Score
.
class Score {
int wins, losses, ties;
}
Imagine we build an online game, and this Score
class has complicated lifecycle requirements. In that case, we should ask the Spring IoC container to manage instances of this class for us.
The Spring container
The following line of code creates and initializes a Spring IoC container that manages the lifecycle of the Score
class:
ApplicationContext spring
= new AnnotationConfigApplicationContext(Score.class);
Notice how the Score.class
is passed into the constructor as the ApplicationContext
is created. This tells Spring to manage instances of the Score
class for us. We don't have to call the Score
class's constructor when we need an instance, but instead we simply ask the Spring IoC container for one:
Score score = spring.getBean(Score.class);
With an instance of the Score
class in hand, we are free to call any of its methods.
score.wins++;
System.out.print(score.wins); // prints 1
Here's all of the code from this example pulled together. When the code runs, it prints out the number 1.
public class SpringIocExample {
public static void main(String[] args) {
ApplicationContext spring
= new AnnotationConfigApplicationContext(Score.class);
// Spring creates and provisions instances. This is the inversion.
Score score = context.getBean(Score.class);
score.wins++;
System.out.print(score.wins); // prints 1
}
}
class Score {
int wins, losses, ties;
}
Inversion of control in action
That example illustrates inversion of control in action -- the Spring container takes responsibility for creating and provisioning instances of the Score
class. We no longer manage the creation and provisioning of instances, but instead that responsibility is inverted and handed over to Spring.
Java constructors vs. Spring IoC
Earlier I suggested that the Spring IoC container should manage beans if they have complex configurations or interesting lifecycles. In the previous example, the Score
class isn't heavily parameterized so configuration isn't an issue, but its lifecycle is managed in an interesting way when the Spring IoC container takes over.
To demonstrate how a Spring-managed bean might behave differently from a bean created by calling its constructor, take a look at the following example where the container-provisioned Score
class is accessed inside a loop:
for (int i=0; i<3; i++) {
Score score = context.getBean(Score.class);
score.wins++;
System.out.print(score.wins);
}
When this code runs, the output is: 1 2 3.
Compare that output to the following code where we provision our own instance of the Score
class by calling the default constructor:
for (int i=0; i<3; i++) {
Score score = new Score();
score.wins++;
System.out.print(score.wins);
}
In this case, the output is 1 1 1, not 1 2 3. When Spring manages the lifecycle of the Score
class, this changes the application's behavior.
Spring bean lifecycle management
Another thing to notice is that the output from the loop proves that the Spring framework manages the lifecycle of the Score in an opinionated way. Rather than create a new instance every time you ask for a Score object, the Spring container just recycles the same instance over and over again.
By default, the Spring container always creates objects using the singleton design pattern. This improves application performance by reducing CPU usage and minimizing the amount of memory the application consumes.
In the singleton pattern, one instance is shared throughout the app; any part of the application that needs the Score is given the same instance. The Spring framework is of the opinion that singleton-based access should be the default behavior of all beans it provisions. That's one of the reasons why Spring is considered an opinionated framework.
Spring is simple
Inversion of control, the key pattern the Spring framework implements, is simple. It simply asserts that any object with complex configuration or a complicated lifecycle should be configured and managed outside of the core business logic.
That is exactly what we showed in this Spring framework example. We asked the Spring container to manage instances of our Score
class for us, and when we needed an instance, the Spring container created and provisioned that managed instance for us.
If you understand that, then you understand the fundamentals of inversion of control and spring.
Cameron McKenzie has been a Java EE software engineer for 20 years. His current specialties include Agile development; DevOps; Spring; and container-based technologies such as Docker, Swarm and Kubernetes.