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.
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:
- Using stereotype annotations, such as
@Component - Using methods annotated with
@Bean - 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
@Primaryresolves ambiguity when multiple beans exist- You can also use
@Qualifierfor 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
@Beanfor External Classes and Custom ConfigurationUse
@Beanmethods 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 RegistrationUse
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:
- Direct
@BeanMethod Calls (aka manual wiring) @BeanMethod Parameters (aka auto-wiring)- Dependency Injection with
@Autowired - Constructor Injection
- Setter Injection
- Field Injection
In @Configuration classes, one @Bean method can call another @Bean method to obtain a dependency.
In @Configuration classes, Spring automatically resolves and injects the required dependencies into the parameters of @Bean methods.
Instead of connecting beans in the @Configuration classes, Spring injects dependencies directly into Spring-managed classes using:
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:
- Field injection (for test code)
- Constructor injection (recommended for production code)
- 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:
- 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.
- 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
@Primaryfor the most common/default bean - Use
@Qualifierwhen 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