Thursday, May 14, 2026

Introduction To Spring Framework

1.0 What is an application framework?

An application framework is a set of reusable components and functionalities that simplifies software development by removing the need for developers to build applications from scratch.

When developing applications, you usually distinguish between boilerplate code and business code.

  • Boilerplate code handles common technical needs shared by many applications, such as error logging, transaction management, security, inter-system communication, and performance optimizations such as caching and data compression. Synonims of boilerplate code are: plumbing code, framework code and infrastructure code.
  • Business code implements the use cases and specific requirements of an application—what users expect the application to do. It is what makes one application different from another.

A framework lets developers focus on business code by providing reusable solutions for common technical needs, reducing the amount of infrastructure code that must be written and maintained.

2.0 Spring as a collection of frameworks

Spring is a collection of frameworks and libraries, not just a single framework; therefore we say that Spring is an ecosystem.

The most common modules in the Spring Framework ecosystem are:

  • Spring Core – the foundational module that provides essential capabilities, including:

    • Spring Context – manages the lifecycle and configuration of objects (beans)
    • Spring AOP (Aspect-Oriented Programming) – intercepts and modifies method execution
    • Spring Expression Language (SpEL) – provides an expression language used in configuration and annotations
  • Spring MVC (Model-View-Controller) – provides the components needed to build web applications and RESTful web services.

  • Spring Data Access – provides support for working with relational databases and data access technologies.

  • Spring Testing – provides tools for writing and running tests for Spring applications.

3.0 Spring Projects

In addition to its core modules and capabilities, the Spring ecosystem includes several important projects, such as:
Spring Data, Spring Security, Spring Cloud, Spring Batch, Spring Boot, etc.
When you develop your application, you can use one or more of these projects.

4.0 Spring Core and the IoC Principle

Spring Framework is built on the Inversion of Control (IoC) principle, one of the core ideas behind the Spring Core module.

  • In a traditional application, the application code is responsible for creating objects, managing their lifecycle, and coordinating how they interact.
  • With IoC, this responsibility is transferred to the framework. Instead of explicitly creating and wiring objects in your code, you provide configuration that tells Spring how the objects should be created and connected. Based on this configuration, Spring instantiates the objects, manages their lifecycle, injects their dependencies, and controls when their methods are invoked.
The term inversion refers to this reversal of responsibility: rather than the application controlling the framework, the framework controls the execution and management of the application components.

5.0 Spring Framework Terminology

Term Definition
Spring Container A general term for the part of the Spring Framework that manages your application's objects, known as beans. It is responsible for creating and configuring beans and wiring them together at runtime.
Application Context The Spring interface used to interact with the Spring container in most Spring Applications. It is represented by the ApplicationContext interface and serves as the central registry where beans are stored and retrieved.
IoC Container A general software engineering term for a component that creates, configures, and manages objects and their dependencies. The Spring container is Spring Framework’s implementation of an IoC container.
Spring Bean An object registered in the Application Context and managed by the Spring container. Once a bean is registered, Spring can inject it into other beans and manage its entire lifecycle.

Why Use Spring Beans?
Instead of creating objects manually with the new keyword throughout your application, you let Spring instantiate and provide them wherever they are needed. This reduces boilerplate code and enables features such as dependency injection, externalized configuration, bean scopes, and lifecycle callbacks. As a result, your application code can focus on business logic rather than object creation and wiring.

6.0 Registering Beans in the Application Context

There are three main ways to register beans in the application context:

  1. Using stereotype annotations, such as @Component
  2. Using methods annotated with @Bean
  3. Using the registerBean() method programmatically

Regardless of how a bean is registered, once it is part of the Spring context, Spring manages it and can inject it into other beans.


6.1 Using Stereotype Annotations

Spring can automatically detect and instantiate classes annotated with stereotype annotations during component scanning.

Common stereotype annotations include: @Component (Generic Spring-managed component) @Service (Service-layer component) @Repository (Data access component) @Controller (Web MVC controller) @RestController (REST API controller).


Example 1. Adding beans using the @Component stereotype

Step 1: Annotate the class

The class Person is now annotated by @Component annotation.

package component;
...

@Component
public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

Step 2: Enable component scanning

The configuration class is annotated with @ComponentScan, which tells Spring which packages to scan in order to automatically detect classes annotated with @Component and create beans for them.

@Configuration
@ComponentScan(basePackages = "component")
public class ProjectConfig {
}

To Retrieve beans from the context, use AnnotationConfigApplicationContext and call getBean():

public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext(ProjectConfig.class);
    Person person = context.getBean(Person.class);
    System.out.println(person.getName()); // null    
}

Example 2. Initializing beans with @PostConstruct

Using @Component stereotype annotations tells Spring to call the default constructor for creating the bean and add it to the context. If you are using the stereotype annotation and want to execute the initialization step, you can use the JavaEE @PostConstruct annotation.

import jakarta.annotation.PostConstruct;

@Component
public class Person {
    private String name;
    @PostConstruct
    public void init() {
        this.name = "John Doe";
    }
    public String getName() {
        return name;
    }
}

Dependency for @PostConstruct

For modern Spring (Spring Boot 3+), use:

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
</dependency>

Note:

  • Older versions used javax.annotation
  • Newer versions use jakarta.annotation

Advantages: Minimal boilerplate. Keeps configuration close to the implementation. Ideal for most application classes.

Disadvantages: Spring controls the instantiation process. By default, only one bean definition is created for each annotated class. Can only be used on classes you own and can modify.


6.2. Using the @Bean Annotation

To define beans manually, apply the @Bean annotation to methods inside a class annotated with @Configuration.
The return value of each method becomes a bean in the Spring context.

Example 1

@Configuration
public class ProjectConfig {
    @Bean
    Person person() {
        var p = new Person();
        p.setName("John Doe");
        return p;
    }
    @Bean
    String hello() {
        return "Hello Spring";
    }
    @Bean
    Integer seven() {
        return 7;
    }
}

To retrieve beans from the context, use the same syntax as before with the AnnotationConfigApplicationContext:

public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext(ProjectConfig.class);
    Person p = context.getBean(Person.class);
    String s = context.getBean(String.class);
    Integer n = context.getBean(Integer.class);
}

When multiple beans of the same type exist, Spring cannot decide which one to inject unless you specify a name or mark one as primary. Note the three different syntaxes to assign a name or identifier to the bean.

Example 2. Defining multiple beans of the same type

@Configuration
public class ProjectConfig {

    @Bean("john")
    Person person1() {
        var p = new Person("John");
        return p;
    }
    @Bean(name = "emma")
    Person person2() {
        var p = new Person("Emma");
        return p;
    }
    @Bean(value = "liam")
    Person person3() {
        var p = new Person("Liam");
        return p;
    }
    @Bean
    @Primary
    Person primaryPerson() {
        var p = new Person("Primary");
        return p;
    }
}

Retrieving beans by name

public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext(ProjectConfig.class);
    Person emma = context.getBean("emma", Person.class);
    Person john = context.getBean("john", Person.class);
    // Retrieves the @Primary bean
    Person primary = context.getBean(Person.class);
}

Important concepts:

  • Bean name = method name (by default) or explicitly defined
  • @Primary resolves ambiguity when multiple beans exist
  • You can also use @Qualifier for more control (advanced usage not shown)

Advantages: Full control over bean creation and initialization. You can create multiple beans of the same type. You can register any object as a bean, including classes from external libraries. You can customize constructor arguments and initialization logic.

Disadvantages: Requires one method per bean, which can lead to repetitive boilerplate code. Configuration classes may become large if many beans are declared manually.


6.3. Using registerBean() Programmatically

Spring provides methods such as registerBean() to register beans dynamically at runtime.


Example: using the context.registerBean() method

This example demonstrates how to add a bean to the Spring application context programmatically at runtime using the method: ApplicationContext.registerBean()

file: ProjectConfig.java

@Configuration
public class ProjectConfig {
}

file: Person.java

package pojo;

public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Person(String name) {
        this.name = name;
    }
}

file: Main.java

package main;

import config.ProjectConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import pojo.Person;
import java.util.function.Supplier;

public class Main {

    public static void main(String[] args) {
        var context = new AnnotationConfigApplicationContext(ProjectConfig.class);
        Supplier<Person> personSupplier = () -> new Person("Michael Jackson");
        context.registerBean("miki", Person.class, personSupplier, pp -> pp.setPrimary(true));
        Person p = context.getBean(Person.class);
        System.out.println(p.getName()); // "Michael Jackson"
    }
}

STEP 1. Creating the Spring Context

var context = new AnnotationConfigApplicationContext(ProjectConfig.class);

This line creates a Spring application context based on the configuration class ProjectConfig.

@Configuration
public class ProjectConfig {
}

Although the configuration class is empty, it is still enough to create a fully functional Spring context. At this point, the context contains only Spring infrastructure beans and no custom application beans.


STEP 2. Creating a Supplier<Person>

Supplier<Person> personSupplier = () -> new Person("Michael Jackson");

Note: the Person object is created using the new keyword and is a plain Java object. The person object becomes a Spring bean only after it is registered in the context.


STEP 3. Registering the Bean

context.registerBean(
        "miki",
        Person.class,
        personSupplier,
        pp -> pp.setPrimary(true)
);

This method registers a new bean in the application context.

Parameters: 1. "miki" = the bean name - 2. Person.class = the bean type - 3. personSupplier = a supplier that returns the actual object - 4. pp -> pp.setPrimary(true) = a customization callback for the bean definition that marks the bean as primary.


STEP 4. Retrieving the Bean

Person p = context.getBean(Person.class);

Advantages: Beans can be registered dynamically based on runtime conditions. Useful in frameworks, libraries, and test setups. Supports custom suppliers and initialization logic.

Disadvantages: More verbose and less declarative. Rarely needed in typical business applications. Can make configuration harder to understand.


6.4. Best Practices for Creating Beans

Choose the bean registration approach based on the level of control you need and whether you can modify the class.

  • Use Stereotype Annotations by Default

    For most application classes, prefer stereotype annotations. This is the simplest and most common way to define beans because it minimizes boilerplate and keeps configuration close to the implementation.

  • Use @Bean for External Classes and Custom Configuration

    Use @Bean methods when you need explicit control over bean creation. Typical situations include: 1) Registering classes from third-party libraries 2) Creating multiple differently configured beans of the same type 3) Applying custom initialization logic. This approach is ideal when the class cannot be annotated directly or when object construction requires additional setup.

  • Use registerBean() for Dynamic Registration

    Use registerBean() only when bean definitions must be created programmatically at runtime. Typical situations include: 1) Dynamic bean registration based on runtime conditions 2) Framework or library development 3) Integration and test configuration. This is an advanced technique and is rarely needed in standard application development.

7 Wiring Beans

Wiring beans refers to providing a bean with a reference to another bean within the application context. In other words, it is how you establish relationships between objects managed by Spring.


7.1 Ways to Wire Beans

There are three main approaches to establishing relationships between beans in the Spring Framework:

  1. Direct @Bean Method Calls (aka manual wiring)
  2. In @Configuration classes, one @Bean method can call another @Bean method to obtain a dependency.

  3. @Bean Method Parameters (aka auto-wiring)
  4. In @Configuration classes, Spring automatically resolves and injects the required dependencies into the parameters of @Bean methods.

  5. Dependency Injection with @Autowired
  6. Instead of connecting beans in the @Configuration classes, Spring injects dependencies directly into Spring-managed classes using:

    • Constructor Injection
    • Setter Injection
    • Field Injection

7.2 Spring Bean Wiring Terminology
Term Definition
Inversion of Control (IoC) A design principle in which the control of object creation, configuration, and lifecycle management is delegated to a container or framework.
Dependency Injection (DI) A design pattern through which Spring realizes (or implements) Inversion of Control. With DI, an object receives its dependencies from an external source, typically the Spring container.
Autowiring A Spring feature that performs dependency injection automatically.
@Autowired A Spring annotation used to enable automatic dependency injection.

Note: other design patterns, such as Business Delegate, may also support certain aspects of IoC. However, Spring primarily realizes IoC through Dependency Injection rather than through the Business Delegate pattern.

7.3 Implementing Relationships Between Beans Defined with @Bean

There are two main approaches when using @Bean methods in a configuration class:

  • Manual wiring (explicit method calls)
  • Auto-wiring (Dependency Injection handled by Spring)

7.3.1. Using the Manual Wiring Approach

With manual wiring, you directly call one @Bean method from the other method.

@Configuration
public class ProjectConfig {

    @Bean
    public Address address() {
        return new Address("Dublin");
    }

    @Bean
    public Person person() {
        Person p = new Person();
        p.setName("Ella");
        p.setAddress(address()); // CALL THE METHOD
        return p;
    }
}

The person() method directly calls address().

Note: although this approach is simple, it tightly couples your configuration methods and is less flexible.


7.3.2. Using the Auto-wiring Approach

With auto-wiring, you let Spring inject the dependency automatically by declaring it as a method parameter.

@Configuration
public class ProjectConfig2 {

    @Bean
    public Address address() {
        return new Address("Dublin");
    }

    @Bean
    public Person person(Address address) { // dependency as parameter
        Person p = new Person();
        p.setName("Ella");
        p.setAddress(address); // set injected dependency
        return p;
    }
}

Spring sees the Address parameter in the person() method and searches the application context for a bean of type Address. Spring automatically injects that bean when creating the Person bean. This auto-wiring mechanism is an example of Dependency Injection (DI).


7.4 Using the @Autowired Annotation to Inject Beans

The @Autowired annotation allows Spring to automatically inject dependencies from the application context into your classes. This approach is typically used with stereotype annotations such as @Component, @Service, or @Repository. Use this approach only when you can modify the class that defines the bean.


Advantages of the @Autowired annotation: Makes relationships between objects explicit in the code. Eliminates manual wiring in configuration classes. When used correctly, improves maintainability and testability.


Ways to Use @Autowired

There are three ways to inject dependencies:

  1. Field injection (for test code)
  2. Constructor injection (recommended for production code)
  3. Setter injection (not recommended)
7.4.1. Field Injection

Both Address and Person are annotated with @Component. The address field in Person is annotated with @Autowired.

package components;

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

@Component
public class Person {

    private String name = "Ella";
    
    @Autowired
    private Address address;
    
    public String getName() {
        return name;
    }
    public Address getAddress() {
        return address;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", address=" + address + "]";
    }    
}

Drawbacks: Cannot use final fields. Harder to write unit tests (requires reflection or Spring context). Hidden dependencies (not visible in constructor).

7.4.2. Constructor Injection (Recommended)

The Person class have a non default constructor, which is annotated by @Autowired, the address field is final.

When the Person bean is created, Spring calls the constructor annotated and injects a bean from the context.

package components;

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

@Component
public class Person {

    private String name = "Ella";
    private final Address address;

    @Autowired // optional if only one constructor
    public Person(Address address) {
        this.address = address;
    }

    public String getName() {
        return name;
    }
    public Address getAddress() {
        return address;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", address=" + address + "]";
    }      
}

Advantages: Supports final fields (immutability). Makes dependencies explicit. Easier to test (can instantiate manually).

Note: If the class has only one constructor, the @Autowired annotation can be omitted.

7.4.3. Setter Injection

Dependencies are injected via a setter method.

package components;

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

@Component
public class Person {

    private String name = "Ella";
    private Address address;

    public String getName() {
        return name;
    }
    public Address getAddress() {
        return address;
    }
    @Autowired
    public void setAddress(Address address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", address=" + address + "]";
    }      
}

Use setter injection: when the dependency is optional. When you need to change the dependency after object creation.

Note: It allows partially initialized objects and therefore is less safe than constructor injection


Using @Autowired is an example of Dependency Injection (DI): instead of creating dependencies yourself, you declare them and Spring provides them.


Example Address class

package components;

import org.springframework.stereotype.Component;

@Component
public class Address {

    private String city = "Dublin";

    public String getCity() {
        return city;
    }
}

Example Config class

package config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "components")
public class ProjectConfig {
}

Best Practices

  • Prefer constructor injection
  • Avoid field injection in production
  • Keep dependencies required and explicit
  • Design classes for immutability when possible

8 How Spring Resolves a Dependency Among Multiple Beans of the Same Class Type

Sometimes, multiple beans of the same class type exist in the Spring context. When Spring needs to inject a dependency, it must determine which bean to use. For example, if two Person beans are defined, how does Spring decide which one to inject?

When multiple beans of the same type are available, Spring resolves the dependency using the following rules:

  1. Match by Parameter or Field Name
    • if the name of a constructor parameter or field matches the name of a bean, Spring injects that bean.
  2. If No Bean Name Matches, you can:
    • mark one bean as @Primary (this defines a default bean)
    • explicitly choose a bean using @Qualifier
    • otherwise, Spring throws an exception because the dependency is ambiguous

Example: Ambiguous Dependency

The following configuration defines two beans of type Person:

@Configuration
public class ProjectConfig {

    @Bean
    public Person person1() {
        Person p = new Person();
        p.setName("John");
        return p;
    }

    @Bean
    public Person person2() {
        Person p = new Person();
        p.setName("Emma");
        return p;
    }
}

If another bean requires a Person, Spring does not know which one to inject and throws an exception.


Example 1: Resolving by Parameter Name

Spring can resolve the dependency if the parameter name matches a bean name.

@Configuration
public class ProjectConfig {

    @Bean
    public Person person1() {
        Person p = new Person();
        p.setName("John");
        return p;
    }

    @Bean
    public Person person2() {
        Person p = new Person();
        p.setName("Emma");
        return p;
    }

    @Bean
    public Team team(Person person2) { // parameter name matches bean name
        Team t = new Team();
        t.setLeader(person2);
        return t;
    }
}

Here, Spring sees the parameter named person2 and injects the bean named person2

Although this works, relying only on parameter names is not always the clearest approach.


Example 2: Using @Qualifier with @Bean

Using @Qualifier is the recommended way to explicitly choose a bean.

@Configuration
public class ProjectConfig2 {

    @Bean
    public Person person1() {
        Person p = new Person();
        p.setName("John");
        return p;
    }

    @Bean
    public Person person2() {
        Person p = new Person();
        p.setName("Emma");
        return p;
    }

    @Bean
    public Team team(@Qualifier("person2") Person person) {
        Team t = new Team();
        t.setLeader(person);
        return t;
    }
}

The @Qualifier("person2") explicitly tells Spring which bean to inject. This approach: improves readability and avoids ambiguity, it is safer during refactoring


Example 3: Using @Qualifier with @Component

The same principle applies when using stereotype annotations such as @Component.

@Component
public class Team {

    private final Person leader;

    @Autowired
    public Team(@Qualifier("person2") Person leader) {
        this.leader = leader;
    }

    public Person getLeader() {
        return leader;
    }
}

Here: Spring injects the bean named person2. The dependency is selected explicitly using @Qualifier.
This is preferable to relying on constructor parameter names.


Best practice:

  • Use @Primary for the most common/default bean
  • Use @Qualifier when a specific bean is required

Example Domain Classes

public class Person {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
public class Team {
    private Person leader;
    public Person getLeader() {
        return leader;
    }
    public void setLeader(Person leader) {
        this.leader = leader;
    }
}

No comments:

Post a Comment