🌱 Spring Core & Dependency Injection
Master Spring Core, Inversion of Control (IoC), Dependency Injection (DI), Spring Beans, AOP, and Java Configuration.
1. Why Spring?
Before the Spring Framework, enterprise Java applications relied heavily on Java EE (J2EE), which was often characterized as heavyweight, complex, and closely coupled to application servers. Developers had to write extensive boilerplate code, and unit testing was extremely difficult because components manually instantiated their dependencies.
Spring solved these problems by introducing Inversion of Control (IoC) and Dependency Injection (DI), leading to loose coupling.
Problems without Spring:
- Tight Coupling: Classes created their own dependencies using the
newkeyword, hardcoding the exact implementation. - Hard to Test: You could not easily swap out real components for mocks in unit tests.
- Boilerplate Code: Performing repetitive tasks (like opening and closing database connections) required hundreds of lines of identical code across classes.
| Feature | Traditional Java EE | Spring Framework |
|---|---|---|
| Complexity | Heavyweight, steep learning curve | Lightweight, POJO-based |
| Testing | Difficult, heavily reliant on containers | Easy, supports plain JUnit tests with mocks |
| Coupling | Tightly coupled components | Loosely coupled via DI |
| Intrusiveness | Highly intrusive (requires extending framework classes) | Non-intrusive (POJOs) |
Spring Framework Modules
The Spring Framework is modular, allowing you to use only the parts you need:
- Core Container: Beans, Core, Context, SpEL (Spring Expression Language). This is the foundation providing IoC and DI.
- AOP & Instrumentation: Supports Aspect-Oriented Programming for cross-cutting concerns (logging, security).
- Data Access / Integration: JDBC, ORM, OXM, JMS, Transactions.
- Web: Web, Web-Servlet (Spring MVC), Web-Reactive (Spring WebFlux), Web-Socket.
- Test: Support for JUnit and TestNG, along with mocking tools.
2. IoC – Inversion of Control
Inversion of Control (IoC) is a principle where the control of object creation, configuration, and lifecycle is delegated to a container or framework rather than the objects themselves. This is often summarized by the Hollywood Principle: "Don't call us, we'll call you."
In Spring, the IoC Container is responsible for instantiating, configuring, and assembling objects known as Beans.
IoC Container Types
Spring provides two main interfaces for its IoC container:
- BeanFactory: The simplest container providing basic DI support. It uses lazy initialization (beans are created only when requested).
- ApplicationContext: A sub-interface of BeanFactory. It adds more enterprise-specific functionality such as event publishing, message resolution (i18n), and application-layer specific contexts. It uses eager initialization for singleton beans.
ApplicationContext Implementations
ClassPathXmlApplicationContext: Loads context definitions from XML files located in the classpath. (Legacy)AnnotationConfigApplicationContext: Loads context definitions from Java-based configuration classes (@Configuration). (Modern approach)GenericWebApplicationContext: Used in web applications to manage web-scoped beans.
3. Dependency Injection (DI)
Dependency Injection is a pattern used to implement IoC, where the container "injects" the required dependencies into an object, rather than the object creating them itself. Spring supports three main types of dependency injection.
A. Constructor Injection (Recommended)
Dependencies are provided through a class constructor. This is the highly recommended approach because it ensures the object is created in a fully initialized state, allows fields to be marked as final, and makes unit testing straightforward without a container.
@Service public class UserService { private final EmailService emailService; // Spring automatically injects EmailService when creating UserService @Autowired // Optional if class has only one constructor public UserService(EmailService emailService) { this.emailService = emailService; } public void registerUser(String username) { // Business logic here... emailService.sendEmail(username, "Welcome to our platform!"); } }
B. Setter Injection
Dependencies are provided through setter methods. This is useful for optional dependencies or when you need to reconfigure dependencies after instantiation. However, it does not guarantee the object is in a valid state after instantiation.
@Service public class NotificationService { private SmsService smsService; @Autowired public void setSmsService(SmsService smsService) { this.smsService = smsService; } }
C. Field Injection (Avoid)
Dependencies are injected directly into fields using the @Autowired annotation. While this is the shortest code, it is highly discouraged. It makes testing difficult (requires reflection or a container), and masks the issue of a class having too many dependencies (violating the Single Responsibility Principle).
@Service public class ReportService { @Autowired private PdfGenerator pdfGenerator; // Field injection - Avoid! }
4. Spring Beans and Scopes
A Spring Bean is simply an object that is instantiated, assembled, and managed by a Spring IoC container. By default, all Spring beans are singletons, meaning only one instance of the bean exists per Spring IoC container.
Bean Scopes
| Scope | Description |
|---|---|
| Singleton (Default) | A single instance per Spring IoC container. |
| Prototype | A new instance is created every time the bean is requested. |
| Request | A new instance per HTTP request (Web contexts only). |
| Session | A new instance per HTTP session (Web contexts only). |
| Application | A single instance per ServletContext (Web contexts only). |
@Component @Scope("prototype") public class ShoppingCart { // A new ShoppingCart instance is created every time it is injected or requested private List<Item> items = new ArrayList<>(); }
5. Stereotype Annotations
Spring provides several "stereotype" annotations to mark classes as Spring-managed components. These annotations make the classes eligible for auto-detection during classpath scanning.
@Component: A generic stereotype for any Spring-managed component.@Service: Specialization of @Component for classes in the service/business logic layer.@Repository: Specialization for data access objects (DAOs). It also automatically translates database-specific exceptions into Spring's unifiedDataAccessExceptionhierarchy.@Controller: Specialization for presentation layer (Web controllers).
// 1. Repository Layer @Repository public class UserRepository { public User findById(long id) { return new User(id, "John Doe"); } } // 2. Service Layer @Service public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } } // 3. Controller Layer @RestController // Combines @Controller and @ResponseBody public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } }
6. @Autowired, @Qualifier, @Primary
When Spring uses @Autowired, it resolves dependencies by type. If there are multiple implementations of an interface, Spring won't know which one to inject, resulting in a NoUniqueBeanDefinitionException. We solve this with @Qualifier or @Primary.
public interface PaymentGateway { void pay(); } @Component("paypal") public class PaypalGateway implements PaymentGateway { ... } @Component("stripe") @Primary // Default if no qualifier is specified public class StripeGateway implements PaymentGateway { ... } @Service public class CheckoutService { private final PaymentGateway defaultPayment; private final PaymentGateway specificPayment; @Autowired public CheckoutService( PaymentGateway defaultPayment, // Injects StripeGateway because of @Primary @Qualifier("paypal") PaymentGateway specificPayment) { // Forces PaypalGateway this.defaultPayment = defaultPayment; this.specificPayment = specificPayment; } }
@Value("${property.name}") to inject configuration values from properties files directly into your beans!
7. Java Configuration (@Configuration)
In legacy Spring, beans were defined in XML files. Modern Spring applications use Java-based configuration with the @Configuration and @Bean annotations.
@Configuration @ComponentScan(basePackages = "com.example") public class AppConfig { // You can manually declare beans instead of using @Component scanning @Bean public DataSource dataSource() { BasicDataSource ds = new BasicDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/mydb"); ds.setUsername("root"); ds.setPassword("password"); return ds; } }
public class Main { public static void main(String[] args) { // Initialize Spring Context from Java configuration ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService service = context.getBean(UserService.class); service.doSomething(); } }
8. Spring AOP (Aspect-Oriented Programming)
AOP is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns (e.g., logging, transaction management, security).
- Aspect: A modularization of a concern that cuts across multiple classes (e.g., a logging module).
- Join Point: A point during the execution of a program, such as a method execution.
- Advice: Action taken by an aspect at a particular join point (Before, After, Around).
- Pointcut: A predicate that matches join points (determining where advice should run).
@Aspect @Component public class LoggingAspect { // Advice: Run around any method execution in the service package @Around("execution(* com.example.service.*.*(..))") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); // Execute the actual method long executionTime = System.currentTimeMillis() - start; System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms"); return proceed; } }
9. Spring Events
Spring provides an event-publishing mechanism that allows you to trigger and listen to custom application events synchronously or asynchronously.
// 1. Custom Event public class UserRegisteredEvent extends ApplicationEvent { private final String username; public UserRegisteredEvent(Object source, String username) { super(source); this.username = username; } public String getUsername() { return username; } } // 2. Publisher @Service public class RegistrationService { @Autowired private ApplicationEventPublisher publisher; public void register(String username) { // Save to DB... publisher.publishEvent(new UserRegisteredEvent(this, username)); } } // 3. Listener @Component public class EmailListener { @EventListener public void handleUserRegistration(UserRegisteredEvent event) { System.out.println("Sending welcome email to: " + event.getUsername()); } }
10. Complete Example: Library Management System
Let's tie it all together with a fully configured Spring Core application using Dependency Injection, Java Configuration, and pure POJOs.
package com.library; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.*; import org.springframework.stereotype.*; // 1. Repository interface BookRepository { void save(String bookName); } @Repository class InMemoryBookRepository implements BookRepository { public void save(String bookName) { System.out.println("Saving book to memory: " + bookName); } } // 2. Service @Service class BookService { private final BookRepository repository; // Constructor Injection @Autowired public BookService(BookRepository repository) { this.repository = repository; } public void addBook(String name) { repository.save(name); } } // 3. Configuration @Configuration @ComponentScan(basePackages = "com.library") class LibraryConfig { } // 4. Main Application public class LibraryApp { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(LibraryConfig.class); BookService service = ctx.getBean(BookService.class); service.addBook("Spring in Action"); } }