At its core, Spring is a Dependency Injection framework. Although it now offers a variety of other features that simplify developers' lives, most of these are built on top of the Dependency Injection framework.
Dependency Injection (DI) is often considered the same as Inversion of Control (IoC). Let’s briefly explain and classify the two terms in the context of the Spring Framework.
Inversion of Control
The concept of Inversion of Control is to give control over the execution of program code to a framework. This can be done, for example, through a function that we program ourselves and then pass to a framework, which then calls it at the right time. We call this function a “callback.”
An example of a callback is a function that should be executed in a server application when a specific URL is called. We program the function, but we do not call it ourselves. Instead, we pass the function to a framework that listens for HTTP requests on a specific port, analyzes the request, and then forwards it to one of the registered callback functions based on specific parameters. The Spring WebMVC project is based on this exact mechanism.
Dependency Injection
Dependency Injection is a specific form of Inversion of Control. As the name suggests, Dependency Injection is about dependencies. Class A
is dependent on another class B
if class A
calls a method of B
. In program code, this dependency is often expressed as an attribute of type B
in class A
:
class GreetingService {
UserDatabase userDatabase = new UserDatabase();
String greet(Integer userId){
User user = userDatabase.findUser(userId);
return "Hello " + user.getName();
}
}
In this example, the GreetingService
requires an object of type UserDatabase
to do its work. When we instantiate an object of type GreetingService
, it automatically creates an object of type UserDatabase
.
The GreetingService
class is responsible for resolving the dependency on UserDatabase
. This is problematic due to several reasons.
First, this solution creates a very strong coupling between the two classes. GreetingService
must know how to create a UserDatabase
object. What if creating a UserDatabase
object is not that simple? To open a database connection, we usually require a few parameters:
class GreetingService {
UserDatabase userDatabase;
public GreetingService(
String dbUrl,
Integer dbPort
){
this.userDatabase = new UserDatabase(dbUrl, dbPort);
}
String greet(Integer userId){
User user = userDatabase.findUser(userId);
return "Hello " + user.getName();
}
}
The GreetingService
still creates its own instance of type UserDatabase
, but now it needs to know which parameters are required for a database connection. The coupling between GreetingService
and UserDatabase
has just become even stronger. We don’t want to see these details in the GreetingService
!
What if other classes in our application also need a UserDatabase
object? We don’t want every class to know how to create a UserDatabase
object!
Due to the strong coupling, details of the UserDatabase
class are spread throughout the codebase. A change to UserDatabase
would therefore lead to many changes in other parts of the code.
This not only makes it difficult to develop application code but also to write tests. If we want to test the GreetingService
class, we need the URL and port of a real database in this example. If we pass invalid connection parameters, the greet()
method no longer works!
To break the strong coupling between classes, we modify the code so that we can “inject” the dependency into the constructor:
class GreetingService {
final UserDatabase userDatabase;
public GreetingService(UserDatabase userDatabase){
this.userDatabase = userDatabase;
}
String greet(Integer userId){
User user = userDatabase.findUser(userId);
return "Hello " + user.getName();
}
}
There is still a coupling between GreetingService
and UserDatabase
, but it is much looser than before because GreetingService
no longer needs to know how a UserDatabase
object is created. The coupling is reduced to the necessary minimum. This pattern is called “constructor injection” since we pass the dependencies of a class in the form of constructor parameters.
In a test, we can now create a mock object of type UserDatabase
(for example, using a mock library like Mockito) and pass it to the GreetingService
. Since we control the behavior of the mock, we no longer need a real database connection to test the GreetingService
class.
In the application code, we instantiate the UserDatabase
class only once and pass this instance to all the classes that need it. In other words, we “inject” the dependency to UserDatabase
into the constructors of other classes.
This “injection” of dependencies can become cumbersome in a real application with hundreds of classes because we need to instantiate all classes in the correct order and explicitly program their dependencies. The result is a lot of “boilerplate” code that often changes and distracts us from the actual development.
This is where Dependency Injection comes into play. A Dependency Injection framework like Spring takes on the task of instantiating most of the classes in our application so that we don’t have to worry about it anymore. It becomes clear that Dependency Injection is a form of Inversion of Control because we hand over control of object instantiation to the Dependency Injection framework.
The division of tasks between Spring and us as developers looks something like this:
- We program the classes
GreetingService
andUserDatabase
. - We express the dependency between the two classes through a parameter of type
UserDatabase
in the constructor ofGreetingService
. - We instruct Spring to take control over the classes
GreetingService
andUserDatabase
. - Spring instantiates the classes in the correct order to resolve dependencies and creates an object network with an object for each passed class.
- When we need an object of type
GreetingService
orUserDatabase
, we ask Spring for that object.
In a real application, Spring manages not just two objects but a complex network of hundreds or thousands of objects. This network is referred to as the “application context” in Spring, as it forms the core of our application.
In the next section, we’ll discuss how Spring’s application context works.
The Spring Application Context
The application context is the heart of an application that is based on the Spring Framework. It contains all the objects whose control we have delegated to Spring. For this reason, it is sometimes also referred to as the “IoC container” (IoC = “Inversion of Control”) or the “Spring container”.
The objects within the application context are called “beans.” If you are not familiar with the Java world, the term “bean” might be somewhat confusing. We can derive the word like this:
- Spring is a framework for the Java programming language.
- Java is the name of an island in Indonesia where coffee is grown, and the type of coffee produced there is also called “Java.”
- Coffee is made from coffee beans.
- In the Java community, it was decided to call certain objects in Java (the programming language) “Beans.”
It’s a bit far-fetched, but the term has become widely adopted, like it or not.
So, the application context is essentially a network of Java objects known as “beans.” Spring instantiates these beans for us and resolves the dependencies between the beans through constructor injection.
But how does Spring know which beans it should create and manage within its application context? This is where the term “configuration” comes into play.
A configuration in the context of Spring is a definition of the beans required for our application. In the simplest case, this is just a list of classes. Spring takes these classes, instantiates them, and includes the resulting objects (beans) in the application context.
If the instantiation of the classes is not possible (for example, if a bean constructor expects another bean that is not part of the configuration), Spring halts the creation of the application context with an exception.
This is one of the advantages that Spring offers: a faulty configuration usually prevents the application from starting at all, thus avoiding potential runtime issues.
There are several ways to create a Spring configuration. In most use cases, it is convenient and practical to program the configuration in Java. However, in cases where the source code should be completely free from dependencies on the Spring Framework, configuring with XML can also be useful.
Configuring an Application Context with XML
In the early days of Spring, the application context had to be configured with XML. XML configuration allows for complete separation of configuration from the code. The code doesn’t need to be aware that it is managed by Spring.
An example XML configuration looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userDatabase" class="de.springboot3.xml.UserDatabase"/>
<bean id="greetingService" class="de.springboot3.xml.GreetingService">
<constructor-arg ref="userDatabase"/>
</bean>
</beans>
In this configuration, the beans userDatabase
and greetingService
are defined. Each bean declaration provides instructions to Spring on how to instantiate that bean.
The class UserDatabase
has a default constructor without parameters, so it is sufficient to provide Spring with the class name. The class GreetingService
has a constructor parameter of type UserDatabase
, so we refer to the previously declared userDatabase
bean using the constructor-ref
element.
With this XML declaration, we can now create an ApplicationContext
object:
public class XmlConfigMain {
public static void main(String[] args) {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext(
"application-context.xml");
GreetingService greetingService =
applicationContext.getBean(
GreetingService.class);
System.out.println(greetingService.greet(1));
}
}
We pass the XML configuration to the constructor of ClassPathXmlApplicationContext
, and Spring creates an ApplicationContext
object for us.
This ApplicationContext
now serves as our IoC container, and, via the method getBean()
we can, for example, inquire about a bean of type GreetingService
from it.
While the XML configuration in this example appears quite manageable, it can become more extensive in larger applications. For us as Java developers, it would be more convenient to manage such a comprehensive configuration in Java itself and take advantage of the Java compiler and IDE features.
Java Configuration in Detail
XML configuration is now mostly used in exceptional cases and legacy applications, and configuration with Java has become the standard. Therefore, let’s take a closer look at this approach, also known as “Java config”.
@Configuration
and @Bean
The core of a Java configuration is a Java class annotated with the Spring annotation @Configuration
:
@Configuration
public class GreetingConfiguration {
@Bean
UserDatabase userDatabase() {
return new UserDatabase();
}
@Bean
GreetingService greetingService(UserDatabase userDatabase) {
return new GreetingService(userDatabase);
}
}
This configuration is equivalent to the XML configuration from the previous section. With the @Configuration
annotation, we inform Spring that this class contributes to the application context. Without this annotation, Spring remains inactive.
A configuration class can declare factory methods like userDatabase()
and greetingService()
, each creating an object. With the @Bean
annotation, we mark such factory methods. Spring finds these methods and calls them to create an ApplicationContext
.
Dependencies between beans, such as the dependency of GreetingService
on UserDatabase
, are resolved through parameters of the factory methods. In our case, Spring first calls the method userDatabase()
to create a UserDatabase
bean and then passes it to the method greetingService()
to create a GreetingService
bean.
Using the AnnotationConfigApplicationContext
class, we can then create an ApplicationContext
:
public class JavaConfigMain {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(GreetingConfiguration.class);
GreetingService greetingService
= applicationContext.getBean(GreetingService.class);
System.out.println(greetingService.greet(1));
}
}
The constructor of AnnotationConfigApplicationContext
allows us to pass multiple configuration classes instead of just one. This is helpful for larger applications because we can split the configuration of many beans into multiple configuration classes to maintain clarity.
@Component
and @ComponentScan
Configuring hundreds or even thousands of beans via Java for a large application can become tedious. To simplify this, Spring offers the ability to “scan” for beans in the Java classpath.
This scanning is activated using the @ComponentScan
annotation:
@Configuration
@ComponentScan("de.springboot3")
public class GreetingScanConfiguration {
}
As before, we create a configuration class (annotated with @Configuration
). However, instead of defining the beans ourselves as factory methods annotated with the @Bean
annotation, we add the new @ComponentScan
annotation.
With this annotation, we instruct Spring to scan the de.springboot3
package for beans. If the scan finds a class annotated with @Component
, it will create a bean from that class (i.e., the class will be instantiated and added to the Spring application context).
Therefore, we simply annotate all classes for which Spring should create a bean with the @Component
annotation:
@Component
public class GreetingService {
private final UserDatabase userDatabase;
public GreetingService(UserDatabase userDatabase) {
this.userDatabase = userDatabase;
}
}
@Component
public class UserDatabase {
// ...
}
As before, dependencies between beans are expressed through constructor parameters, and Spring resolves these automatically.
@Bean vs. @Component
The annotations @Bean
and @Component
express a similar concept: both mark a contribution to the Spring application context. This similarity can be confusing, especially at the beginning.
The Java compiler helps here a bit, as the @Bean
annotation is only allowed on methods, and the @Component
annotation is only allowed on classes. So, we cannot confuse them. However, we can still annotate methods and classes that Spring doesn’t recognize!
Spring evaluates the @Bean
annotation only within a @Configuration
class, and the @Component
annotation only on classes found by a component scan.
Combining @Configuration
and @ComponentScan
Spring does not dictate how we should configure the beans of our application. We can configure them using XML or Java config or even combine both approaches. We can also combine explicit bean definitions using @Bean
methods with a scan using @ComponentScan
:
@Configuration
@ComponentScan("de.springboot3.java.mixed")
class MixedConfiguration {
@Bean
GreetingService greetingService(UserDatabase userDatabase) {
return new GreetingService(userDatabase);
}
}
// no @Component annotation!
class GreetingService {...}
@Component
class UserDatabase {...}
In this configuration, Spring creates a bean of type UserDatabase
because the class is annotated with @Component
, and a @ComponentScan
is configured. On the other hand, the bean of type GreetingService
is defined through the explicit @Bean
-annotated factory method.
Modular Configuration
Configuring a larger application with hundreds of beans can quickly become confusing.
The explicit configuration using @Bean
annotations has the advantage that the configuration of beans is bundled in a few @Configuration
classes and is easy to understand.
The implicit configuration using @ComponentScan
and @Component
has the advantage that we don’t need to define each bean ourselves, but it can be spread over many @Component
annotations and, therefore, over the entire codebase, making it more challenging to grasp.
A proven principle is to design the Spring configuration along the architecture of the application. Each module of the application should reside in its own package and have its own @Configuration
class. In this @Configuration
class, we can either configure a @ComponentScan
for the module package or use explicit configuration using @Bean
methods. To bring the modules together into a complete application, we create a parent @Configuration
class that defines a @ComponentScan
for the main package. This scan will pick up all @Configuration
classes in this and the sub-packages.
What benefits does the Spring Container give us?
Now we know that Spring offers us an IoC container that instantiates and manages objects (beans) for us. This saves us from the burden of managing the lifecycle of these objects ourselves.
But that’s just the beginning. Since Spring has control over all beans, it can perform many other tasks for us. For example, Spring can intercept calls to bean methods to start or commit a database transaction. We can also use Spring as an event bus. We send an event to the Spring container, and Spring forwards the event to all interested beans.
We will delve into these and many other features throughout the rest of the book. The foundation of all these features is the Spring programming model, whose core we have already learned in this chapter. This programming model is a combination of annotations, conventions, and interfaces that we can assemble into a complete application.
Events
Since Spring manages all beans defined by us, the framework can send messages to these beans. We can use this to develop an event mechanism that loosely couples our components. This is just one of the benefits that the Spring container gives us.
Loose Coupling
When a software component requires the functionality of another component, the two components are “coupled” together.
This coupling can vary. For example, a class may call a method of another class to access its functionality. This couples the two classes at compile time. We refer to this as “strong coupling.”
The strength of the coupling also depends on how extensive and complicated the signature of the called method is – if it’s a simple method without parameters, the coupling is not as strong as if the method expects a series of complex parameters. Once we make a change to the types of these parameters, we have to modify the code of both the calling and the called class.
Coupling is not inherently bad. Sometimes, two classes need to work very closely together to fulfill a function.
Most of the time, we want to design our code modularly. When working on a module, we don’t want to have to think about all the other modules of the application. This is only possible if the dependencies between the modules are limited.
Loosely coupled modules allow for parallel development. Each module can be developed by a different developer or even team. This is not possible if the modules are heavily coupled because then the teams would step on each other’s feet.
Let’s take the example of a banking application that implements use cases from two different domains. The first module implements the “User” domain. It manages user data and serves as the single source of truth for this data. Another module implements the “Transactions” domain. This module implements functions related to money transfers.
Every time the “Transactions” module initiates a transfer, it needs to check whether the user initiating the transfer is locked or not. The transfer is only executed if the user is not locked.
The “Transactions” module could always directly call the “User” module before performing a transfer and ask if the user is locked or not. However, this would strongly couple the “Transactions” module to the “User” module. Whenever we make a change to the “User” module, we may also have to adjust the code in the “Transactions” module. Additionally, in the future, we may want to extract the “Transactions” module into its own (micro)service so that it can be released independently of the “User” module.
Events provide a solution to loosely couple both modules. Every time a new user is registered, locked, or unlocked, the “User” module sends an event. The “Transactions” module listens to these events and updates its own database with the current status of each respective user. Before performing a transfer, the “Transactions” module can now check its own database to see if the user is locked or not, instead of having to make a request to the user module. The data storage is completely decoupled from the “User” module.
Using this example, we want to explore how we can implement such an event mechanism with Spring and Spring Boot.
Sending Events
The prerequisite for sending and receiving events with Spring is that both the sender and the receiver must be registered as beans in the ApplicationContext
.
We can define the events themselves as simple Java classes or records. For example, we can write our user events as follows:
public record UserCreatedEvent(User user) {}
public record UserLockedEvent(int userId) {}
public record UserUnlockedEvent(int userId) {}
To send such an event, we can use the ApplicationEventPublisher
interface. Conveniently, Spring automatically provides a bean that implements this interface in the ApplicationContext
. So we can simply inject it into our UserService
:
@Component
public class UserService {
private final ApplicationEventPublisher applicationEventPublisher;
public UserService(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
public void createUser(User user) {
// ... business logic omitted
this.applicationEventPublisher.publishEvent(new UserCreatedEvent(user));
}
public void lockUser(int userId) {
// ... business logic omitted
this.applicationEventPublisher.publishEvent(new UserLockedEvent(userId));
}
public void unlockUser(int userId) {
// ... business logic omitted
this.applicationEventPublisher.publishEvent(new UserUnlockedEvent(userId));
}
}
We simply call the publishEvent()
method. That’s all we need to do to send an event.
Receiving Events
There are several ways to respond to events in Spring. We can implement the ApplicationListener
interface or use the @EventListener
annotation.
ApplicationListener
The conventional way to respond to an event in a Spring application is by implementing the ApplicationListener
interface:
@Component
public class UserCreatedEventListener implements ApplicationListener<UserCreatedApplicationEvent> {
private static final Logger logger
= LoggerFactory.getLogger(UserCreatedEventListener.class);
private final TransactionDatabase database;
public UserCreatedEventListener(TransactionDatabase database) {
this.database = database;
}
@Override
public void onApplicationEvent(UserCreatedApplicationEvent event) {
this.database.saveUser(new User(event.getUser().id(), User.UNLOCKED));
logger.info("received event: {}", event);
}
}
The UserCreatedEventListener
class is part of the “Transactions” module. It has access to an object of type TransactionDatabase
, which simulates the module’s database.
We implement the onApplicationEvent()
method, which, in our case, takes an object of type UserCreatedApplicationEvent
. The event contains a User
object. The listener takes the user’s data that the “Transactions” module needs and saves it in the database. Users are unlocked by default, so we pass the UNLOCKED
status.
Why do we use the event class UserCreatedApplicationEvent
instead of the record UserCreatedEvent
we learned about earlier?
Because Spring’s ApplicationListener
can only handle events of type ApplicationEvent
. This means that every event we send must inherit from this class:
public class UserCreatedApplicationEvent extends ApplicationEvent {
private final User user;
public UserCreatedApplicationEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
}
As we can see, it is somewhat cumbersome to receive events with an ApplicationListener
. On the one hand, our event must inherit from the ApplicationEvent
class, and on the other hand, we must implement the ApplicationListener
interface, which can only respond to a single type of events. To receive other event types, we would have to write additional ApplicationListener
s or implement a large if/else block in the onApplicationEvent()
method.
The Spring team recognized this and added an annotation-based event mechanism to Spring.
@EventListener
To respond to an event, we can also use the @EventListener
annotation. If Spring finds this annotation on a method of a bean registered in the ApplicationContext
, Spring automatically sends all events of the corresponding type to this method.
@Component
public class UserEventListener {
private final TransactionDatabase database;
public UserEventListener(TransactionDatabase database) {
this.database = database;
}
@EventListener(UserCreatedEvent.class)
public void onUserCreated(UserCreatedEvent event) {
this.database.saveUser(new User(event.user().id(), User.UNLOCKED));
}
}
The UserEventListener
class is added to the Spring ApplicationContext
using the @Component
annotation. The onUserCreated()
method is annotated with @EventListener
and accepts an object of our simple record type UserCreatedEvent
.
The event does not have to inherit from the ApplicationEvent
type, as in the example with an ApplicationListener
! Spring internally wraps our event in an ApplicationEvent
, but we don’t notice it. We can receive any type as an event, but immutable records are best suited for transporting events.
Synchronous or asynchronous?
If we use events as shown in the examples, our “Users” and “Transactions” modules are decoupled at compile time. The only coupling between the modules are the event objects, which both modules must know.
But how are our modules coupled at runtime? What happens if our EventListener
in the “Transactions” module produces an exception when receiving an event or takes a long time to process an event? Can and should we react to this in our “Users” module?
By default, the Spring Event mechanism works synchronously, as shown in the following diagram:
The UserService
sends an event to the ApplicationEventPublisher
. This knows which event listeners are interested in the event, and calls them one after the other (in our case, it calls only the UserEventListener
). Only when the onUserCreated()
method has completed its processing does the control flow return to the UserService
. The processing takes place synchronously.
This means that if the UserEventListener.onUserCreated()
method throws an exception, it arrives in the UserService
and interrupts the processing there. Or if the method takes a long time, the control flow in the UserService
is interrupted for that long.
So our modules are not as decoupled as we had hoped! We have to adapt our exception handling and make our code more robust so that it can handle long waiting times if necessary.
Can we also process events asynchronously to reduce this coupling, as shown in the following sequence diagram?
The short answer is “yes.” We can simply annotate the listener method with the @Async
annotation:
@Component
public class UserEventListener {
@Async
@EventListener(UserCreatedEvent.class)
public void onUserCreated(UserCreatedEvent event) {
// …
}
}
This prompts Spring to return control flow directly to the caller and execute the method in a separate thread in the background.
For the @Async
annotation to take effect, we must first activate it. We do this using the @EnableAsync
annotation on one of our @Configuration
classes.
Since events are now processed asynchronously, exceptions in the event listener are no longer forwarded to the sender of the event. Depending on the use case, this may be desired or undesired.
We should be aware that decoupling using the @Async
annotation only scales to a limited extent. If we process a large number of events, they will accumulate in Spring’s internal thread pool and be processed one after the other. In synchronous event processing, scaling problems become apparent more quickly, as the entire processing chain slows down instead of just the event processing.
Spring Boot’s Application Events
During the lifecycle of an application, Spring Boot sends some events that we can respond to. Most of these events are very technical and are rarely relevant to us as application developers. The following table briefly lists these events in chronological order:
Event | Description |
---|---|
ApplicationStartingEvent | The application is currently starting, but the Environment and ApplicationContext are not yet available. |
ApplicationEnvironmentPreparedEvent | The application is currently starting, and the Spring Environment is available. |
ApplicationContextInitializedEvent | The application is currently starting, and the Spring ApplicationContext is available. |
ApplicationPreparedEvent | A combination of the previous two events. The ApplicationContext and Environment are available. |
ContextRefreshedEvent | The ApplicationContext has been updated. This happens when the ApplicationContext starts and when it is reloaded (for example, after a configuration change). |
WebServerInitializedEvent | The embedded web server (Tomcat by default) has started. |
ApplicationStartedEvent | The application has started, but no ApplicationRunners and CommandLineRunners have been executed yet. |
ApplicationReadyEvent | The application is fully started. |
ApplicationFailedEvent | The application did not start due to an error. |
The most relevant event for us is probably the ApplicationReadyEvent
, as it is fired when the application is ready to work. We can use it, for example, to activate certain components in our application.
If our application processes messages from a queue, for example, we want to make sure that our application is also ready to process these messages. This is the case as soon as we have received the ApplicationReadyEvent
.
We cannot react to some of the earlier events from the table in the “normal” way, as they are fired very early. If we want to react to these events, we must manually register an ApplicationListener
:
@SpringBootApplication
public class EventsApplication {
public static void main(String[] args) {
SpringApplication springApplication =
new SpringApplication(EventsApplication.class);
springApplication.addListeners(new MyApplicationListener());
springApplication.run(args);
}
}