[Pre-W2] Beans and Dependency Injection

YC Tech Academy Backend Career Project

Overview

This is a pre-study for week 2. The main topic for this week is beans and dependency injection in Spring Boot. We will be going over the basic understanding of beans, the core of Spring Applications, and how dependency injection works for beans.

What are beans?

ChatGPT explains beans as such:

Beans are the fundamental components in the Spring Framework, representing objects managed by the Spring IoC (Inversion of Control) container. They are instantiated, configured, and assembled by the container based on configurations provided by the developer, either through XML, annotations, or Java code. Beans facilitate the building of modular and testable applications by promoting loose coupling and separation of concerns through dependency injection.

There is a lot to unpack here, so let's slowly digest this.

IoC (Inversion of Control)

Inversion of Control (IoC) is a software design principle where the control of object creation, configuration, and lifecycle is shifted from the application's code to an external framework or container. Instead of the application code actively managing these aspects, the container injects the required dependencies and manages the objects.

IoC has some strong advantages:

  1. Modularity & Reusability: IoC supports the development of independent and reusable components, enhancing scalability and maintainability.

  2. Flexibility & Configurability: Components can be easily swapped, replaced, or configured without changing the core logic.

  3. Testability: With dependency injection, real implementations can be substituted with mock objects, simplifying testing.

  4. Decoupling & Separation of Concerns: Components focus on their core responsibilities, leading to a system with minimal interdependencies and cleaner code.

  5. Consistency: Centralized management ensures a uniform approach to object creation and configuration.

Code Example

In traditional coding, we will connect object Item by doing this:

public class Store {
    private Item item;

    public Store() {
        item = new ItemImpl1();    
    }
}

However, with IoC, the item will be connected via this:

public class Store {
    private Item item;
    public Store(Item item) {
        this.item = item;
    }
}

As seen from above, the item is connected to the private Item variable through the constructor. This is called injection or dependency injection. By doing this, item no longer depends on ItemImpl1, and it becomes a more flexible code.

Spring IoC Container

Now we understand IoC, it is time to understand what Spring IoC Container. An IoC container is a common characteristic of frameworks that implement IoC.

It provides a consistent mechanism to configure and manage Java objects using reflection. These objects are Spring beans, and they are created based on configurations provided by the developer, either through XML, annotations, or Java code.

The Spring container handles the lifecycle, configuration, and wiring of these beans, ensuring they're available where needed in the application. The primary interfaces for the Spring IoC container are BeanFactory and ApplicationContext, with the latter being a super-interface of the former and offering more advanced features.

Code Example

Let's say there is a service that says hello:

package com.example.service;

public class GreetingService {
    public String sayHello() {
        return "Hello, Spring IoC Container!";
    }
}

To use this as bean, there needs to be a configuration that applies this as a bean:

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.service.GreetingService;

@Configuration
public class AppConfig {

    @Bean
    public GreetingService greetingService() {
        return new GreetingService();
    }
}
  • @Configuration marks AppConfig as a configuration class.

  • @Bean marks GreetingService as a bean so that Spring IoC container knows that GreetingService class is a bean that needs management.

Now this AppConfig is used to instantiate a Spring IoC container by the following:

package com.example;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        GreetingService greetingService = context.getBean(GreetingService.class);
        System.out.println(greetingService.sayHello());
    }
}
  • ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    This creates the Spring IoC container, allowing us to access beans for other projects.

  • GreetingService greetingService = context.getBean(GreetingService.class);

    This creates a Bean object for Greeting service. The getBean method returns an instance of the specified class, and Spring ensures that this instance is properly configured with all its dependencies.

  • greetingService.sayHello(): This line is calling the sayHello method on the greetingService bean that we just fetched from the Spring context.

Summary

To summarize, beans are type of objects managed by Spring Framework, and by using IoC, we can loosely connect objects for better coding experience. We call this Bean Injection.

Bean Components

Now, as we start programming in Spring Boot, we will encounter this thing called components. Components are general-purpose stereotype annotation indicating that a class is a Spring-managed component.

When you annotate a class with @Component, it tells Spring that this class is a candidate for Spring's component scanning to detect and automatically register it as a bean in the Spring application context. This process is often referred to as "auto-detection" or "auto-wiring".

There are specialized forms of the @Component annotation that cater to specific layers or semantics in an application:

  • @Repository: For persistence layers (typically used with database repositories).

  • @Service: For service layers (business logic).

  • @Controller (or @RestController): For presentation layers, especially in Spring MVC applications.

Injection types

There are multiple ways we can inject beans. The example from above is a type of a constructor injection. There is also an injection type called field injection. Both have similarities and differences, which makes them used in different contexts.

Field Injection

In field injection, the Spring container injects the dependency directly into the class field, bypassing the constructor or setter methods. This is done using the @Autowired annotation on the field itself.

Pros:

  • Less boilerplate code since you don't need setter methods or constructors.

Cons:

  • The main disadvantage is that you can't make the injected fields final, which means they can be modified later, potentially breaking immutability.

  • Harder to write unit tests for the class since you can't easily provide mock dependencies without using reflection or Spring-specific testing utilities.

Code Example:

package com.example.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    @Autowired
    private MessageService messageService;

    public String notify(String message) {
        return messageService.sendMessage(message);
    }
}

Constructor Injection

In constructor injection, the Spring container injects the dependencies through the class constructor. This is done using the @Autowired annotation on the constructor. In recent versions of Spring, if a class has only one constructor, the @Autowired annotation can be omitted, and Spring will use that constructor by default.

Pros:

  • You can make the injected fields final, ensuring immutability.

  • Easier to write unit tests since you can provide mock dependencies through the constructor without needing Spring.

  • Explicitly shows required dependencies.

Cons:

  • Requires more boilerplate code if there are multiple dependencies.

Code Example:

package com.example.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    private final MessageService messageService;

    @Autowired
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public String notify(String message) {
        return messageService.sendMessage(message);
    }
}

@Primary and @Qualifier annotation

As we start working with beans and dependency injections, there are times when beans collide, and it becomes important to determine which is more important. This is where @Primary and @Qualifier annotation comes in.

@Primary

When you have multiple beans of the same type, and you want one of them to be the default choice for injection, you can use the @Primary annotation.

Pros:

  • Provides a centralized way to set the default bean.

  • Reduces the need to use @Qualifier everywhere.

Cons:

  • Only one bean of a particular type can be marked as @Primary.

Example:

@Service
@Primary
public class EmailService implements MessageService {
    // ...
}

@Service
public class SMSService implements MessageService {
    // ...
}

@Service
public class NotificationService {
    private final MessageService messageService;

    @Autowired
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;  // EmailService will be injected by default
    }
}

@Qualifier

When you have multiple beans of the same type and you want to specify exactly which bean to inject, you can use the @Qualifier annotation followed by the name of the desired bean.

Pros:

  • Provides fine-grained control over bean selection.

  • Useful when multiple beans of the same type exist, and none of them is a default choice.

Cons:

  • Requires specifying the bean name, which can lead to hard-coded strings in the code.

Example:

@Service("emailService")
public class EmailService implements MessageService {
    // ...
}

@Service("smsService")
public class SMSService implements MessageService {
    // ...
}

@Service
public class NotificationService {
    private final MessageService messageService;

    @Autowired
    public NotificationService(@Qualifier("smsService") MessageService messageService) {
        this.messageService = messageService;  // SMSService will be injected
    }
}

Hands-on Tutorial with H2 database

I have tried to make H2 Database, but for some reason, I am unable to do so with my computer. After long hours of trying, I have decided to seek help from the lecturers. I will report on this when I figure out what my problem was.

Conclusion

Navigating through the world of Spring Framework, particularly around beans and dependency injection, can initially feel like traversing a maze. This guide has broken down the core concepts, from understanding the essence of Spring beans and their relationship with the IoC container, to diving into the various annotations like @Component, @Autowired, @Primary, and @Qualifier that guide how beans are managed and injected. Additionally, we've touched on the importance of constructor and field injections, weighing their pros and cons.

While our hands-on tutorial on the H2 database faced hiccups, it's a testament to the very nature of software development: learning through challenges and persistence. As we head into week 2, let this pre-study serve as a foundation and reference point. Remember, the depth and intricacies of Spring Boot's ecosystem are vast, but with each layer peeled back, we gain clarity and expertise. As you continue your journey, may the knowledge here not only aid your coding tasks but inspire deeper exploration into the vast sea of Spring Boot capabilities.

Happy coding!