Dependency injection (DI) is a cornerstone of modern software development, promoting loose coupling and making applications easier to test and maintain.
However, many DI frameworks come with a significant amount of complexity, configuration overhead, and runtime reflection, which can bloat applications and slow down startup times.
For developers who value performance and simplicity, finding the right tool can be a challenge. Enter Avaje Inject, a DI library designed specifically to address these issues.
It offers a fresh, compile-time approach to dependency injection that emphasizes speed, simplicity, and a near-zero-reflection footprint.
If you’re looking for a powerful yet lightweight DI solution that embraces Java simplicity, this guide will walk you through what makes Avaje Inject a compelling choice for your next project.
We will explore its core features, from its intuitive annotations to its seamless integration with popular Java modules.
Getting Started with Avaje Inject
Integrating Avaje Inject into your project is straightforward. It leverages modern Java Platform Module System (JPMS) principles and familiar build tool configurations to get you up and running quickly.
Maven Configuration
To add Avaje Inject to a Maven project, you need to include two main dependencies in your pom.xml file: the avaje-inject artifact and the avaje-inject-generator annotation processor.
The generator is the key to Avaje Inject’s compile-time magic, as it creates the necessary wiring at build time.
Here is a typical Maven setup:
- avaje-inject: The core library containing the necessary annotations and runtime components.
- avaje-inject-generator: The annotation processor that scans your code and generates the DI implementation.
<dependencies>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
<version>9.1</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>9.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
Gradle Configuration
For Gradle users, the setup is similar. You’ll add the core library to your dependencies block and configure the annotation processor.
dependencies {
implementation ‘io.avaje:avaje-inject:9.1’
annotationProcessor ‘io.avaje:avaje-inject-generator:9.1’
}
With these dependencies in place, your build tool will automatically trigger the annotation processor, which will generate the DI code needed to wire your application together.
Core Concepts and Annotations
Avaje Inject follows the Jakarta Inject standard, using familiar annotations that make the learning curve minimal for developers experienced with other DI frameworks.
Its simplicity shines through with a concise set of annotations for defining and injecting beans.
Defining Beans
You can register a class as a bean using standard annotations. Avaje Inject supports:
- @Singleton: This marks a class for which only one instance should be created. It’s the most common scope.
- @Component: Similar to @Singleton, this is a general-purpose annotation for marking a component.
- @Factory: Use this on a class to indicate that it contains methods that produce beans. This is useful for integrating third-party libraries or when construction logic is complex.
Creating Beans with Factories
Factory classes are a powerful feature for managing bean creation. By annotating a class with @Factory, you can define methods that produce beans.
@Factory
public class AppConfig {
@Bean
public MyService createMyService() {
// Complex initialization logic
return new MyServiceImpl();
}
}
Injecting Dependencies
Injecting a dependency is done using the standard @Inject annotation on a constructor. Constructor injection is the recommended approach as it ensures that objects are created in a valid and complete state.
@Singleton
public class MyController {
private final MyService myService;
@Inject
public MyController(MyService myService) {
this.myService = myService;
}
}
This focus on standard annotations and clear patterns helps maintain Java simplicity while providing robust DI capabilities.
The Power of Compile-Time DI
The most significant advantage of Avaje Inject is its compile-time approach.
Unlike traditional DI frameworks that use runtime reflection to discover and wire beans, Avaje Inject generates plain Java code during the compilation phase.
How it Works
When you build your project, the avaje-inject-generator annotation processor scans your codebase for annotations like @Singleton, @Factory, and @Inject.
Based on this information, it generates the necessary Java classes to construct and wire your application’s object graph. This generated code is then compiled along with the rest of your application.
Key Benefits
This compile-time strategy delivers several key benefits:
- Faster Startup: Since all the dependency wiring is resolved at compile time, there is no need for classpath scanning or reflection at runtime. This leads to significantly faster application startup times, which is crucial for microservices and serverless functions.
- Early Error Detection: Any issues with your dependency graph, such as missing dependencies or circular references, are detected during compilation. This means you get immediate feedback in your IDE or build tool, rather than discovering a NullPointerException at runtime.
- Minimal Reflection: The generated code is simple, direct Java. This avoids the performance overhead and complexities associated with reflection, resulting in a more efficient and predictable application.
This approach makes Avaje Inject a truly lightweight DI solution, as the runtime component is minimal and highly optimized.
Modular Application Design
Avaje Inject is designed with the Java Platform Module System (JPMS) in mind, making it an excellent choice for building modular applications.
External Modules
You can easily create external modules that provide a set of beans. To do this, create a module that contains your @Singleton or @Factory classes. Other modules can then depend on this module to gain access to its beans.
Controlling Bean Visibility
With JPMS, you can control which beans are exposed to other modules. A bean is only available to other modules if:
- Its package is exported by its module (exports com.example.myapi;).
- The bean itself is public.
This allows you to create well-encapsulated modules with a clear public API, preventing other modules from accessing internal implementation details.
provides Directive
For even better modularity, a module can explicitly declare the services it provides using the provides directive in its module-info.java file.
This makes the dependency graph clearer and helps enforce architectural boundaries.
// in module-info.java
module my.service.provider {
requires io.avaje.inject;
exports com.example.myapi;
provides io.avaje.inject.spi.Module with com.example.my.generated.GeneratedModule;
}
This deep integration with JPMS helps developers build robust, maintainable, and truly modular systems.
Advanced Features and Integrations
Beyond the basics, Avaje Inject offers several advanced features that enhance its flexibility and power.
Qualifiers with @Named
When you have multiple beans of the same type, you can use the @Named annotation to distinguish between them. This allows you to inject a specific implementation where needed.
// In a Factory
@Bean
@Named(“first”)
public MyService firstService() { … }
@Bean
@Named(“second”)
public MyService secondService() { … }
// Injecting a specific bean
@Inject
public MyController(@Named(“first”) MyService service) {
this.service = service;
}
Lifecycle Management with @PostConstruct and @PreDestroy
Avaje Inject supports standard lifecycle annotations for managing bean initialization and cleanup.
- @PostConstruct: A method annotated with @PostConstruct will be called after the bean has been created and all its dependencies have been injected.
- @PreDestroy: This annotation marks a method to be called just before the bean is removed from the container, allowing for graceful shutdown and resource release.
Seamless Integration with Avaje Config
For configuration management, Avaje Inject integrates smoothly with Avaje Config.
This allows you to inject configuration values directly into your beans using the @Config annotation, further simplifying application setup.
Testing Your Application
The design of Avaje Inject makes testing simple and efficient. Because it promotes constructor injection, you can easily instantiate your classes in unit tests by manually providing mock or stub dependencies.
Unit Testing
For a simple unit test, you don’t need the DI container at all. Just create an instance of the class you want to test and pass in mock objects.
@Test
void testMyController() {
MyService mockService = mock(MyService.class);
when(mockService.doSomething()).thenReturn(“mocked value”);
MyController controller = new MyController(mockService);
// … assertions
}
Integration Testing with @TestScope
For integration tests, Avaje Inject provides a @TestScope annotation. This allows you to build a test-specific dependency injection context where you can easily add mock or spy beans.
You can use @Mock and @Spy annotations directly in your test classes to replace real beans with test doubles.
This streamlined approach to testing ensures that you can maintain high code quality and confidence in your application without the complexity of traditional testing setups.
Embrace Lean Dependency Injection
Avaje Inject provides a compelling alternative to traditional DI frameworks by focusing on compile-time code generation, modern Java modularity, and adherence to standards.
Its lightweight DI nature results in faster startup times and a smaller runtime footprint, while its embrace of Java simplicity makes it easy to learn and use.
Detecting dependency issues at compile time helps developers build more robust and reliable applications.
If your team is looking to reduce bloat and complexity in your Java applications without sacrificing the power of dependency injection, it’s time to give Avaje Inject a serious look.
Explore the official documentation and try integrating it into your next project to experience the benefits of a lean, fast, and developer-friendly DI solution.
