Skip to content

Introduction to Spring: The Spring IoC Container

Introduction

Spring has evolved rapidly throughout the years. Released in 2002, it revolutionized the way Java developers build enterprise software.

It is one of those frameworks that developers love to have on their tech stack as it allows them to add new features to their software without worrying about configuration.

So What Is Spring?

The Spring Framework is a powerful open-source, application framework, and inversion of control (IoC) container created to reduce the complexity of enterprise application development. Its’ layered architecture allows you to use what you need and leave which you don’t.

Spring Framework has many technologies involved in it, but in this article, we’re going to focus on its’ most important in my opinion: the Spring IoC container. It’s the first concept someone should understand when entering the Spring world.

Spring IoC Container

Before explaining how the Spring Container works, let’s give a basic definition of Inversion of Control (IoC) and Dependency Injection (DI).

Defining Inversion of Control (IoC)

Inversion of control is a software engineering principle by which the control of objects is transferred to a framework. In other words, the program receives the flow of control from the framework. It’s mostly used in object-oriented programming.

Defining Dependency Injection (DI)

UML Class and Sequence diagrams which describe how the dependency injection pattern works (from W3Design)

Inversion of control can be achieved in many ways. One of them is dependency injection (DI). In dependency injection, objects receive other objects that they depend on. These other objects are called dependencies. Their creation is not hard-coded. It’s up to the IoC container to manage the objects’ life cycle and make the necessary wirings.

Through dependency injection, we achieve separation of concerns (SoC). This increases readability and code reuse.

Dependency injection in Spring is usually achieved in 3 ways, namely:

  • Constructor Injection
  • Setter Injection
  • Field Injection

Each of these methods has its’ advantages and disadvantages, but this discussion is for another post.

The @Autowired annotation in Spring makes injection easy. Just put it on the constructor/setter/field and most occasions you’re ready to go!

Defining Beans

Before explaining the Spring IoC container we must explain the term bean. Spring’s documentation is giving an excellent definition:

“In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.”

Spring’s class-level bean declaration annotations

There are 2 mainstream methods of bean declaration by using annotations:

  • With @Bean at method-level
  • With the generic @Component and its’ specialties @Repository, @Service, and @Controller at class-level

How The Spring IoC Container Works

The Spring IoC container (taken from Spring Docs)

The Spring IoC container manages the complete life cycle of the application’s beans. It creates beans, wires them together (using dependency injection), and configures them. We control the container by providing metadata to it. This metadata can be provided either by XML, Java annotations, or Java code. In 2020, we are trying to get away from XML and instead use annotations or code.

The container is accessible at runtime through the org.springframework.context.ApplicationContext interface.

Bean Scopes

Beans have scopes. The scope of a bean defines its’ life cycle and visibility in the contexts in which it is used.

As of now, Spring defines 6 types of scopes:

  • Singleton
  • Prototype
  • Request
  • Session
  • Application
  • Websocket

We configure a bean’s scope by using the @Scope annotation. A bean’s scope defaults to the singleton type.

The last 4 scopes mentioned are usually used when developing web applications. Hence, we’re going to focus on the first 2.

This Baeldung’s post gives excellent definitions for each bean scope. I’m going to quote what it says about the singleton and prototype scopes:

Singleton:

“Defining a bean with singleton scope means the container creates a single instance of that bean, and all requests for that bean name will return the same object, which is cached. Any modifications to the object will be reflected in all references to the bean. This scope is the default value if no other scope is specified.”

Prototype:

“A bean with prototype scope will return a different instance every time it is requested from the container.”

Sample Application

We are going to demonstrate what we learned through a sample application. We will build a simple booking system that will focus on showing how the IoC container works rather than having functionality.

NOTE: It is not recommended nowadays to create a new plain Spring project in production. Spring Boot is a Spring module that simplifies Spring development a lot by removing a lot of configurations and dependencies. It is not used here so that the project conforms to the article’s contents.

Spring-5.2.7.RELEASE will be used. A newer release version may be available when reading that article, but this project will probably be playable on future versions too.

The Models

Let’s make some Java POJOs that are going to represent our domain models.

Client.java

public class Client
{
    private final Integer id;
    private String name;
    private String surname;
    private Integer age;
    private static int idCounter = 1;
    public Client(String name, String surname, Integer age)
    {
        //Automatically assign id. Faking auto-incremental
        this.id = idCounter++;
        this.name = name;
        this.surname = surname;
        this.age = age;
    }
    //Getters
    //Setters
    //Custom toString()
    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Client client = (Client) o;
        return id.equals(client.id);
    }
    @Override
    public int hashCode()
    {
        return Objects.hash(id);
    }
}

Booking.java

public class Booking
{
    private final Integer id;
    private Client client;
    private String hotel;
    private Integer nights;
    private static int idCounter = 1;
    public Booking(Client client, String hotel, Integer nights)
    {
        //Automatically assign id. Faking auto-incremental
        this.id = idCounter++;
        this.client = client;
        this.hotel = hotel;
        this.nights = nights;
    }
    //Getters
    //Setters
    //Custom toString()
    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Booking booking = (Booking) o;
        return id.equals(booking.id);
    }
    @Override
    public int hashCode()
    {
        return Objects.hash(id);
    }
}

The Beans

To avoid complexity, we are not going to use a database system. Instead, our data is going to be stored in Lists with each list respectively holding an entity we created earlier. These will be our beans. We will let the Spring IoC container handle their lifecycle as well as injecting them wherever necessary to resolve dependencies.

First off, let’s supply the bean metadata to the IoC container. We need a class annotated with @Configuration for it:

Config.java

@Configuration
public class Config
{
    @Bean(name = "clientDB")
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
    public List<Client> getClientDB()
    {
        System.out.println("Created a new Client list on " + LocalDateTime.now());
        return Collections.synchronizedList(new ArrayList<>());
    }
    @Bean(name = "bookingDB")
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
    public List<Booking> getBookingDB()
    {
        System.out.println("Created a new Booking list on " + LocalDateTime.now());
        return Collections.synchronizedList(new ArrayList<>());
    }
}

Notice that we gave both beans a name. In cases where the container is going to manage multiple beans of the same type (in this case the 2 lists), wiring by class type isn’t enough, because the container doesn’t know which bean to choose. We need to specify by name the bean we want the container to wire.

The Repositories

NOTE: All the below boilerplate repository code can be easily be avoided in a normal application using Spring Data JPA. It could not be used here because Spring Data JPA requires working with databases.

Let’s create a generic interface which will hold the contract of how a repository should look like. It will have 2 type parameters: 

  • T for the model type, and
  •  ID for the primary key type

CrudRepository.java

public interface CrudRepository<T, ID>
{
    List<T> findAll();
    Optional<T> findById(ID id);
    void save(T item);
    void update(T item);
    void delete(T item);
}

Now, let’s create the 2 repositories which will implement the above interface:

ClientsRepository.java

@Repository
public class ClientsRepository implements CrudRepository<Client,Integer>
{
    private List<Client> clientDB;
    @Override
    public List<Client> findAll()
    {
        return Collections.unmodifiableList(clientDB);
    }
    @Override
    public Optional<Client> findById(Integer id)
    {
        Objects.requireNonNull(id);
        return  clientDB.stream()
                .filter(x-> x.getId().equals(id))
                .findFirst();
    }
    @Override
    public void save(Client client)
    {
        Objects.requireNonNull(client);
        clientDB.add(client);
    }
    @Override
    public void update(Client client)
    {
        Objects.requireNonNull(client);
        clientDB.set(clientDB.indexOf(client),client);
    }
    @Override
    public void delete(Client client)
    {
        Objects.requireNonNull(client);
        clientDB.remove(client);
    }
    @Autowired
    @Qualifier("clientDB")
    public void setClientDB(List<Client> clientDB)
    {
        this.clientDB = clientDB;
    }
}

BookingsRepository.java

@Repository
public class BookingsRepository implements CrudRepository<Booking,Integer>
{
    private List<Booking> bookingDB;
    @Override
    public List<Booking> findAll()
    {
        return Collections.unmodifiableList(bookingDB);
    }
    @Override
    public Optional<Booking> findById(Integer id)
    {
        Objects.requireNonNull(id);
        return  bookingDB.stream()
                .filter(x-> x.getId().equals(id))
                .findFirst();
    }
    @Override
    public void save(Booking booking)
    {
        Objects.requireNonNull(booking);
        bookingDB.add(booking);
    }
    @Override
    public void update(Booking booking)
    {
        Objects.requireNonNull(booking);
        bookingDB.set(bookingDB.indexOf(booking),booking);
    }
    @Override
    public void delete(Booking booking)
    {
        Objects.requireNonNull(booking);
        bookingDB.remove(booking);
    }
    @Autowired
    @Qualifier("bookingDB")
    public void setBookingDB(List<Booking> bookingDB)
    {
        this.bookingDB = bookingDB;
    }
}

The Service

Now, let’s implement some business logic. We want the client to be able to make a booking by giving the client’s ID, the name of the hotel the client wants to stay, and the number of the nights they are going to stay.

We will create a service which wires the 2 repositories we created earlier and applies the required logic to make a booking

BookingsService.java

@Service
public class BookingsService
{
    private ClientsRepository clientsRepository;
    private BookingsRepository bookingsRepository;
    public void makeBooking(int clientID, String hotelName, int nights)
    {
        Optional<Client> client = clientsRepository.findById(clientID);
        if(client.isPresent())
            bookingsRepository.save(new Booking(client.get(),hotelName,nights));
        else
            System.err.println("Error: Client with ID: "+ clientID + " does not exist");
    }
    @Autowired
    public void setClientsRepository(ClientsRepository clientsRepository)
    {
        this.clientsRepository = clientsRepository;
    }
    @Autowired
    public void setBookingsRepository(BookingsRepository bookingsRepository)
    {
        this.bookingsRepository = bookingsRepository;
    }
}

Testing Time

In this section, we are going to run some console application examples which will showcase what we previously saw.

Singleton vs Prototype Scope

We have this console application:

public class SingletonVSPrototypeExample
{
    public static void main(String[] args)
    {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.coderapper");
        List<Client> db1 = context.getBean("clientDB",List.class);
        db1.add(new Client("Am I","A Singleton?",1));
        System.out.println("DB 1 contents: " + db1.toString());
        List<Client> db2 = context.getBean("clientDB",List.class);
        System.out.println("DB 2 contents: " + db2.toString() + '\n');
    }
}

In line 5, we supply configuration metadata to the IoC container by scanning all the classes of the app. Then, we can access the container through context.

The clientDB bean in Config.java is configured to have singleton scope through @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON). So we expect db1 and db2 to point to the same reference of the bean. The following output confirms that:

Created a new Booking list on 2020-07-12T14:00:00.187704
Created a new Client list on 2020-07-12T14:00:00.196967
DB 1 contents: [Client{id=1, name='Am I', surname='A Singleton?', age=1}]
DB 2 contents: [Client{id=1, name='Am I', surname='A Singleton?', age=1}]

What would happen if the bean had prototype scope? Simply put, the container would create a new instance of the bean every time that bean was requested from it.

Let’s change the scope of the clientDB bean :

    @Bean(name = "clientDB")
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public List<Client> getClientDB()
    {
        System.out.println("Created a new Client list on " + LocalDateTime.now());
        return Collections.synchronizedList(new ArrayList<>());
    }

The new output is:

Created a new Booking list on 2020-07-12T14:06:36.873376
Created a new Client list on 2020-07-12T14:06:36.882415
Created a new Client list on 2020-07-12T14:06:36.892029
DB 1 contents: [Client{id=1, name='Am I', surname='A Singleton?', age=1}]
Created a new Client list on 2020-07-12T14:06:36.913435
DB 2 contents: []

You may wonder why 3 client lists were created. Don’t forget that ClientsRepository also requests the clientDB bean.

Adding A Client

We revert the scope of the clientDB from prototype to singleton again.

We now have this console application:

public class App
{
    public static void main(String[] args)
    {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.coderapper");
        ClientsRepository clientsRepository = context.getBean(ClientsRepository.class);
        clientsRepository.save(new Client("Code","Rapper",19));
        System.out.println("Clients DB Contents: " + clientsRepository.findAll());
    }
}

Here, we are using ClientsRepository to save a new client. Then, we request to fetch all the saved clients. The output is:

Created a new Booking list on 2020-07-12T14:20:56.342699
Created a new Client list on 2020-07-12T14:20:56.354023
Clients DB Contents: [Client{id=1, name='Code', surname='Rapper', age=19}]

Making A Booking

We expand the console app to this:

public class MakeBookingExample
{
    public static void main(String[] args)
    {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.coderapper");
        ClientsRepository clientsRepository = context.getBean(ClientsRepository.class);
        clientsRepository.save(new Client("Code","Rapper",19));
        System.out.println("Clients DB Contents: " + clientsRepository.findAll());
        BookingsService bookingsService = context.getBean(BookingsService.class);
        bookingsService.makeBooking(1,"Super Hotel",5);
        BookingsRepository bookingsRepository = context.getBean(BookingsRepository.class);
        System.out.println("Bookings DB Contents: " + bookingsRepository.findAll());
    }
}

Here, we use the BookingsService to make a new booking. Then, we are using the BookingsRepository to fetch all the saved bookings. The output is:

Created a new Booking list on 2020-07-12T14:50:37.591962
Created a new Client list on 2020-07-12T14:50:37.601963
Clients DB Contents: [Client{id=1, name='Code', surname='Rapper', age=19}]
Bookings DB Contents: [Booking{id=1, client=Client{id=1, name='Code', surname='Rapper', age=19}, hotel='Super Hotel', nights=5}]

Conclusion

In this article, we’ve summarized what Spring is and how the Spring IoC Container works. The full source code of the examples is over on GitHub.

Published inSpring
5 3 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments