DependencyInjector
Lightweight, simple, and modern dependency injection framework for Java featuring constructor-based injection and automatic post-construction method invocation
Install / Use
/learn @neziw/DependencyInjectorREADME
🚀 DependencyInjector
</div>Lightweight, simple, and modern dependency injection framework for Java featuring constructor-based injection and automatic post-construction method invocation
📋 Table of Contents
- Features
- Requirements
- Why Field-Based Injection is Bad?
- Installation
- Quick Start
- Configuration
- API Reference
- Annotations
- Contributing
- License
✨ Features
- 🔌 Constructor Injection - Simple and intuitive constructor-based dependency injection
- 🎯 Annotation-Driven - Clean and declarative
@Injectand@PostConstructannotations - 🚀 Lightweight - Minimal dependencies, zero runtime overhead
- 🔧 Easy Integration - Simple API, easy to integrate into any Java project
- 🛡️ Type-Safe - Compile-time type safety with Java generics
- 📝 Post-Construction - Automatic invocation of
@PostConstructmethods after object creation - 🎨 Clean Code - Encourages clean, testable, and maintainable code architecture
- ⚡ Fast - Reflection-based implementation with minimal overhead
📦 Requirements
- Java: 17 or higher
- Build Tool: Maven or Gradle (for dependency management)
❌ Why Field-Based Injection is Bad?
This framework intentionally supports only constructor-based injection and does not provide field-based injection. This is a deliberate design decision based on best practices and software engineering principles. Here's why field-based injection is problematic and why constructor injection is the superior approach:
🔴 Problems with Field-Based Injection
1. Immutability and Final Fields
Field-based injection requires non-final fields, making your objects mutable and potentially leaving them in an inconsistent state:
// ❌ BAD: Field injection
public class UserService {
@Inject // Field must be non-final
private DatabaseService databaseService; // Can be null, can be changed
public void saveUser(User user) {
// What if databaseService is null? No way to enforce it at compile time
this.databaseService.save(user);
}
}
With constructor injection, you can use final fields, ensuring immutability and thread-safety:
// ✅ GOOD: Constructor injection
public class UserService {
private final DatabaseService databaseService; // Final, immutable, thread-safe
@Inject
public UserService(final DatabaseService databaseService) {
this.databaseService = databaseService; // Guaranteed to be non-null
}
public void saveUser(User user) {
this.databaseService.save(user); // Always available
}
}
2. Testability Issues
Field-based injection makes unit testing significantly more difficult. You must use reflection or rely on the dependency injection framework even in tests:
// ❌ BAD: Testing with field injection
public class UserServiceTest {
@Test
void testSaveUser() {
UserService userService = new UserService(); // databaseService is null!
// Must use reflection or a mock framework to inject
// Reflection.setField(userService, "databaseService", mockDatabase);
// This is error-prone and fragile
}
}
Constructor injection makes testing straightforward and explicit:
// ✅ GOOD: Testing with constructor injection
public class UserServiceTest {
@Test
void testSaveUser() {
DatabaseService mockDatabase = mock(DatabaseService.class);
UserService userService = new UserService(mockDatabase); // Clean and explicit
// Test implementation
}
}
3. Hidden Dependencies
Field-based injection hides dependencies. When you look at a class, you cannot immediately see what dependencies it requires without examining annotations and fields. This makes code harder to understand and maintain:
// ❌ BAD: Hidden dependencies
public class OrderService {
@Inject
private PaymentService paymentService; // Hidden dependency
@Inject
private ShippingService shippingService; // Hidden dependency
@Inject
private EmailService emailService; // Hidden dependency
// Looking at this class, it's not immediately clear what dependencies are needed
// Must scan all fields to understand the class dependencies
}
Constructor injection makes dependencies explicit and visible:
// ✅ GOOD: Explicit dependencies
public class OrderService {
private final PaymentService paymentService;
private final ShippingService shippingService;
private final EmailService emailService;
@Inject
public OrderService(
final PaymentService paymentService,
final ShippingService shippingService,
final EmailService emailService
) {
this.paymentService = paymentService;
this.shippingService = shippingService;
this.emailService = emailService;
}
// Dependencies are immediately visible in the constructor signature
// Easy to understand what this class needs
}
4. Circular Dependency Detection
Field-based injection can hide circular dependencies until runtime, making them harder to detect and debug. Constructor injection exposes circular dependencies immediately, forcing you to resolve them during design:
// ❌ BAD: Circular dependency hidden with field injection
public class ServiceA {
@Inject
private ServiceB serviceB; // Circular dependency not obvious
}
public class ServiceB {
@Inject
private ServiceA serviceA; // Circular dependency not obvious
}
// This might work at runtime but creates tight coupling and design issues
Constructor injection makes circular dependencies impossible, encouraging better design:
// ✅ GOOD: Circular dependency impossible with constructor injection
// If you try to create ServiceA, you need ServiceB
// If you try to create ServiceB, you need ServiceA
// This immediately reveals the design problem and forces you to refactor
5. Null Safety
Field-based injection can leave objects in an invalid state where required dependencies are null. There's no compile-time guarantee that dependencies are injected. Constructor injection ensures that all required dependencies are provided before the object is created:
// ❌ BAD: Null safety issues
public class UserService {
@Inject
private DatabaseService databaseService; // Could be null!
public void saveUser(User user) {
// Runtime NullPointerException if injection failed
this.databaseService.save(user);
}
}
Constructor injection guarantees non-null dependencies:
// ✅ GOOD: Null safety guaranteed
public class UserService {
private final DatabaseService databaseService; // Final, guaranteed non-null
@Inject
public UserService(final DatabaseService databaseService) {
// Compiler and framework ensure this is never null
this.databaseService = Objects.requireNonNull(databaseService);
}
}
6. Framework Coupling
Field-based injection tightly couples your code to the dependency injection framework. Your classes cannot be instantiated without the framework, making them harder to reuse and test. Constructor injection allows classes to be instantiated normally, with or without the framework:
// ❌ BAD: Tight framework coupling
public class UserService {
@Inject
private DatabaseService databaseService;
// Cannot create UserService without the DI framework
// Must use reflection or framework-specific mechanisms
}
Constructor injection provides flexibility:
// ✅ GOOD: Framework-agnostic
public class UserService {
private final DatabaseService databaseService;
@Inject // Optional: Framework can use this
public UserService(final DatabaseService databaseService) {
this.databaseService = databaseService;
}
// Can still be created normally: new UserService(databaseService)
// Framework is optional, not required
}
7. Order of Initialization
Field-based injection makes the order of initialization unclear. Dependencies might be injected in an unpredictable order, leading to initialization issues. Constructor injection ensures a clear, predictable initialization order:
// ❌ BAD: Unclear initialization order
public class ServiceA {
@Inject
private ServiceB serviceB;
@PostConstruct
void init() {
// Is serviceB injected before this runs? Unclear
this.serviceB.doSomething();
}
}
Constructor injection provides a clear sequence: constructor → field assignment → @PostConstruct methods:
// ✅ GOOD: Clear initialization order
public class ServiceA {
private final ServiceB serviceB;
@Inject
public ServiceA(final ServiceB serviceB) {
// 1. Constructor runs first
this.serviceB = serviceB; // 2. Fields assigned
}
@PostConstruct
void init() {
// 3. Post-construct runs last, all dependencies guaranteed available
this.serviceB.doSomething();
}
}
✅ Why Constructor Injection is Superior
Constructor injection provides numerous benefits that field injection cannot match:
- Immutability - Enables
finalfields, ensuring objects are immutable and thread-safe - Explicit Dependencies - Dependencies are visible in the constructor signature, making code self-documenting
- Testability -
