Avaje Inject

Fast and light dependency injection library for Java and Kotlin developers.

License Source API Docs Issues Releases
Apache2 Github Javadoc Github Latest 8.8

 

Uses Java annotation processing generating source code for dependency injection. That puts the work of performing dependency injection to build time rather than runtime increasing the speed of application start. There is no use of reflection or classpath scanning.

DI is generated source code allowing developers to see how it works and we can use existing IDE tools to search where code is called such as constructors and lifecycle methods. We can add debug breakpoints into the DI code as desired and step through it just like it was manually written code.

For a background on why avaje inject exists and a quick comparison with other DI libraries such as Dagger2, Micronaut, Quarkus and Spring goto - Why.

DI Library size

Do we care about the size of a DI library? Why is dagger and avaje-inject so much smaller?

avaje-inject exists with the view that it can be really small and provide JSR-330 dependency injection using source code generation.

avaje-inject includes:

DI library size comparison

Releases for javax and jakarta

The move of JEE to the eclipse foundation meant a change in package from javax to jakarta for various APIs including JSR-330 dependency injection API.

Today we have the choice of using the javax.inject dependency or using the new jakarta.inject dependency.

Want to use javax.inject?

Use version 7.8 of avaje-inject with the dependency on javax.inject (maintained on javax.main branch).

Want to use jakarta.inject?

Use version 8.8 of avaje-inject with the dependency on jakarta.inject (maintained on master branch).

Both version 8.x and 7.x require Java 11. For Java 8 support use versions 6.22 (for jakarta.inject) or 5.22 (for javax.inject).

Based on JSR-330

avaje inject is based on JSR-330: Dependency Injection for Java - javax.inject / jakarta.inject with some extensions similar to Spring DI.

JSR-330 provides:

JSR 250 - Common Annotations for the Java provides:

@PostConstruct and @PreDestroy are part of Common Annotations API. These where in JDK 8 but from JDK 9 onwards are part of JDK module javax.annotation-api.

Noting that at this point neither Dagger2 or Guice support @PostConstruct and @PreDestroy lifecycle annotations.

Spring DI like extensions to JSR-330

@InjectTest - component testing

We use @InjectTest for component testing which is similar to Spring's junit extensions SpringExtension, @SpringBootTest.

This is where we get avaje-inject to wire the test class using @Inject, @Mock, @Spy.

@Factory + @Bean

In addition to the JSR-330 standard we use @Factory + @Bean which work the same as Spring DI's @Configuration + @Bean and also Micronaut's @Factory + @Bean. This is also similar to a Guice module with @Provides methods.

Teams will often use @Factory + @Bean to provide dependencies that are best created programmatically. Typically, these depend on external configuration, environment settings etc.

Factory provides a more convenient alternative to the JSR-330 javax.inject.Provider<T> interface and is also more natural for people who are familiar with Spring DI or Micronaut DI.

@Primary + @Secondary

Additionally we use @Primary @Secondary annotations which work the same as Spring DI's @Primary + @Secondary and also Micronaut DI's @Primary + @Secondary. These provide injection priority in the case when multiple injection candidates are available.

Dependencies

Maven

See the basic example at: examples/basic-di/pom.xml

Add avaje-inject as a dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject</artifactId>
  <version>8.8</version>
</dependency>

Add avaje-inject-generator annotation processor as a dependency with provided scope.

<!-- Annotation processor -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject-generator</artifactId>
  <version>8.8</version>
  <scope>provided</scope>
</dependency>

Note that if there are other annotation processors and they are specified via maven-compiler-plugin annotationProcessorPaths then we add avaje-inject-generator there instead.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessorPaths> <!-- All annotation processors specified here -->
      <path>
          <groupId>io.avaje</groupId>
          <artifactId>avaje-inject-generator</artifactId>
          <version>8.8</version>
      </path>
      <path>
          ... other annotation processor ...
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

Gradle

See the example at: examples/javalin-gradle-java-basic/build.gradle

Gradle 5.2+

Use Gradle version 5.2 or greater which has better support for annotation processing.

Dependencies

Add avaje-inject as an implementation dependency, avaje-inject-generator as an annotation processor, and avaje-inject-test as a test dependency.

dependencies {
  ...
  implementation('io.avaje:avaje-inject:8.8')
  annotationProcessor('io.avaje:avaje-inject-generator:8.8')

  testImplementation('io.avaje:avaje-inject-test:8.8')
  testAnnotationProcessor('io.avaje:avaje-inject-generator:8.8')
}

Kotlin KAPT

See example at: https://github.com/dinject/examples/blob/master/basic-di-kotlin-maven/pom.xml

For use with Kotlin we register avaje-inject-generator as a kapt processor to the Kotlin compiler rather than annotationProcessor.

dependencies {
  ...
  implementation('io.avaje:avaje-inject:8.8')
  kapt('io.avaje:avaje-inject-generator:8.8')

  testImplementation('io.avaje:avaje-inject-test:8.8')
}

Java modules - module-info

If using java modules, in the module-info.java we need to:

  1. Add a requires clause for io.avaje.inject
  2. Add a provides clause for io.avaje.inject.spi.Module
Example module-info
module org.example {
  requires io.avaje.inject;
  provides io.avaje.inject.spi.Module with org.example.ExampleModule;
}

In the example above, org.example.ExampleModule is generated code typically found in target/generated-sources/annotations.

Injection

@Singleton

Put @Singleton on beans that we want dependency injection on. These are beans that are created ("wired") by dependency injection and put into the scope. They are then available to be injected into other beans.

@Singleton
public class CoffeeMaker {
  ...
Spring DI Note

Spring @Component, @Service and @Repository are all singleton scoped. With avaje-inject these would map to @Singleton.

@Component

@Component is similar to JSR-330 @Singleton except it is avaje-inject specific. In general, we prefer to use the JSR-330 standard annotations but there are a couple of cases where would choose to use the avaje-inject specific @Component instead.

In these cases we may choose to use the avaje-inject specific @Component rather than JSR-330 @Singleton.

@Component
public class CoffeeMaker {
  ...

Ignoring @Singleton

To get avaje-inject to ignore any classes annotated with @Singleton use:

@InjectModule(ignoreSingleton = true)

With ignoreSingleton = true avaje-inject will ignore @Singleton with the view that some other DI library is also being used and is handling those components.

@Inject

Put @Inject on the constructor that should be used for constructor dependency injection. Note that if there is only one constructor we don't need to put the @Inject on it.

If we want to use field injection put the @Inject on the field. Note that the field must not be private and must not be final for field injection.

Constructor injection

@Singleton
public class CoffeeMaker {

  private final Pump pump;

  private final Grinder grinder;

  @Inject
  public CoffeeMaker(Pump pump, Grinder grinder) {
    this.pump = pump;
    this.grinder = grinder;
  }
  ...

The above CoffeeMaker is using constructor injection. Both a Pump and Ginder will be injected into the constructor when the DI creates (or "wires") the CoffeeMaker.

If there is only 1 constructor it is used for dependency injection and we don't need to specify @Inject.

In general we do not expect to see logic in constructors as this typically makes it more difficult to write tests. If we see logic in a constructor then it is likely that we should try and move that logic to a Factory method instead.

Kotlin constructor

With Kotlin we frequently will not specify @Inject with only one constructor. The CoffeeMaker constructor injection then looks like:

@Singleton
class CoffeeMaker(private val pump: Pump , private val grinder: Grinder)  {

  fun makeCoffee() {
    ...
  }
}

Field injection

@Singleton
public class CoffeeMaker {

  @Inject
  Pump pump;

  @Inject
  Grinder grinder;
  ...

With field injection the @Inject is placed on the field and the field must not be private and it must not be final.

Constructor injection preferred

Generally there is a preference to use constructor injection over field injection as constructor injection:

Circular dependencies

We use field injection or method injection to handle circular dependencies. See below for more details.

Kotlin field injection

For Kotlin we can consider using lateinit on the property with field injection.

@Singleton
class Grinder {

  @Inject
  lateinit var pump: Pump

  fun grind(): String {
    ...
  }
}

Method injection

For method injection annotate the method with @Inject.

@Singleton
public class CoffeeMaker {

  Grinder grinder;

  @Inject
  void setGrinder(Grinder grinder) {
    this.grinder = grinder;
  }
  ...

Mixed constructor, field and method injection

We are allowed to mix constructor, field and method injection. In the below example the Grinder is injected into the constructor and the Pump is injected by field injection.

@Singleton
public class CoffeeMaker {

  @Inject
  Pump pump;

  private final Grinder grinder;

  public CoffeeMaker(Grinder grinder) {
    this.grinder = grinder;
  }

Circular dependencies

When we have a circular dependency then we need to use either field injection or method injection on one of the dependencies.

For example, lets say we have A and B where A depends on B and B depends on A. In this case we can't use constructor injection for both A and B like:

// circular dependency with constructor injection, this will not work !!

@Singleton
class A {
  B b;
  A(B b) {       // constructor for A depends on B
    this.b = b;
  }
}

@Singleton
class B {
  A a;
  B(A a) {       // constructor for B depends on A
    this.a = a;
  }
}

With the above circular dependencies for A and B constructor injection avaje-inject can not determine the order in which to construct the beans. avaje-inject will detect this and product a compilation error outlining the beans involved and ask us to change to use field injection for one of the dependencies.

We can not use constructor injection for both A and B and instead we must use either field injection or method injection on either A or B like:

@Singleton
class A {
  @Inject   // use field injection
  B b;
}

@Singleton
class B {
  A a;
  B(A a) {
    this.a = a;
  }
}

The reason this works is that field injection and method injection occurs later after all the dependencies are constructed. avaje-inject uses 2 phases to "wire" the beans and then a 3rd phase to execute the @PostConstruct lifecycle methods:

Circular dependencies more commonly occur with more than 2 beans. For example, lets say we have A, B and C where:

With A, B, C above they combine to create a circular dependency. To handle this we need to use field injection or method injection on one of the dependencies.

Optional

We can use java.util.Optional<T> to inject optional dependencies. These are dependencies that might not be provided / might not have an available implementation / might only be provided based on configuration (a bit like a feature toggle).

@Singleton
class Pump {

  private final Heater heater;

  private final Optional<Widget> widget;

  @Inject
  Pump(Heater heater, Optional<Widget> widget) {
    this.heater = heater;
    this.widget = widget;
  }

  public void pump() {
    if (widget.isPresent()) {
      widget.get().doStuff();
    }
    ...
  }
}
Spring DI Note

Spring users will be familiar with the use of @Autowired(required=false) for wiring optional dependencies. With avaje-inject we instead use Optional or @Nullable to inject optional dependencies.

@Nullable

As an alternative to Optional we can use @Nullable to indicate that a dependency is optional / can be null. Any @Nullable annotation can be used, it does not matter which package the annotation is in.

@Singleton
class Pump {

  private final Heater heater;

  private final Widget widget;

  @Inject
  Pump(Heater heater, @Nullable Widget widget) {
    this.heater = heater;
    this.widget = widget;
  }

  public void pump() {
    if (widget != null) {
      widget.doStuff();
    }
    ...
  }
}

List

We can inject a java.util.List<T> of beans that implement an interface.

@Singleton
public class CombinedBars {

  private final List<Bar> bars;

  @Inject
  public CombinedBars(List<Bar> bars) {
    this.bars = bars;
  }

Set

We can inject a java.util.Set<T> of beans that implement an interface.

@Singleton
public class CombinedBars {

  private final Set<Bar> bars;

  @Inject
  public CombinedBars(Set<Bar> bars) {
    this.bars = bars;
  }

Provider

A Singleton bean can implement javax.inject.Provider<T> to create a bean to be used in injection.

@Singleton
class FooProvider implements Provider<Foo> {

  private final Bazz bazz;

  FooProvider(Bazz bazz) {
    this.bazz = bazz;
  }

  @Override
  public Foo get() {
    // maybe do interesting logic, read environment variables ...
    return new BasicFoo(bazz);
  }
}

We can then have another bean that has Provider<T> injected into it. It calls get() to get an instance to then use.

@Singleton
class UseFoo  {

  private final Provider<Foo> fooProvider;

  UseFoo(Provider<Foo> fooProvider) {
    this.fooProvider = fooProvider;
  }

  void doStuff() {

    // get a Foo instance and use it
    Foo foo = fooProvider.get();
    ...
  }
}

When using Provider<T> get() we can get a new instance each time we call get(). In the above example, the FooProvider.get() method returns a new instance each time get() is called. This is effectively the same as Prototype scope.

Note that the alternative to implementing the Provider<T> interface is to instead use @Factory and @Bean as it is more flexible and convenient than implementing the provider interface.

Spring DI Note

The JSR 330 javax.inject.Provider<T> interface is functionally the same as Spring DI FactoryBean<T>.

@Prototype

@Factory

Factory beans allow us to programmatically creating a bean. Often the logic is based on external configuration, environment variables, system properties etc.

We annotate a class with @Factory to tell us that it contains methods that create beans. The factory class can itself have dependencies and the methods can also have dependencies.

@Factory @Bean are equivalent to Spring DI @Configuration @Bean and Micronaut @Factory @Bean. Guice users will see this as similar to Modules with @Provides methods.

@Bean

We annotate methods on the factory class that create a bean with @Bean. These methods can have dependencies and will execute in the appropriate order depending on the dependencies they require.

Example

@Factory
class Configuration {

  private final StartConfig startConfig;

  /**
   * Factory can have dependencies.
   */
  @Inject
  Configuration(StartConfig startConfig) {
    this.startConfig = startConfig;
  }

  @Bean
  Pump buildPump() {
    // maybe read System properties or environment variables
    return new FastPump(...);
  }

  /**
   * Method with dependencies as method parameters.
   */
  @Bean
  CoffeeMaker buildBar(Pump pump, Grinder grinder) {
    // maybe read System properties or environment variables
    return new CoffeeMaker(...);
  }
}

@Bean initMethod & destroyMethod

With @Bean we can specify an initMethod which will be executed on startup like @PostConstruct. Similarly a destroyMethod which execute on shutdown like @PreDestroy.

Example

@Factory
class Configuration {
  ...
  @Bean(initMethod = "init", destroyMethod = "close")
  CoffeeMaker buildCoffeeMaker(Pump pump) {
    return new CoffeeMaker(pump);
  }
}

The CoffeeMaker has the appropriate methods that are executed as part of the lifecycle.

class CoffeeMaker {

  void init() {
    // lifecycle executed on start/PostConstruct
  }
  void close() {
    // lifecycle executed on shutdown/PreDestroy
  }
  ...
}

Optional @Bean

We can use Optional<T> to indicate that the method produces an optional dependency.

Often the dependency is only provided based on external configuration a bit like a feature toggle / config toggle. For example, we might do this in a CI/CD environment until such time that the dependency is always "ON" in all environments and then we change to make the dependency not optional.

Example - Optional dependency

@Factory
class Configuration {

  /**
   * Optionally provide MessageQueue.
   */
  @Bean
  Optional<MessageQueue> buildQueue() {
    if (...) { // maybe read external config etc
      // Not providing the dependency (kind of like feature toggle)
      return Optional.empty();
    }
    return Optional.of(...);
  }

}

Use of @Factory @Bean

It is good to use @Factory for all the dependencies we want to create programmatically. Many teams will have a standard location/package they use to put a "configuration factory bean" where all programmatically created dependencies are defined as a general approach.

If we see logic in constructors then we typically would try to move that logic to a factory bean method and keep the constructors simple. Logic in constructors typically makes it harder from a testing perspective.

@Primary

A bean with @Primary is deemed to be highest priority and will be injected and used when it is available. This is functionally the same as Spring and Micronaut @Primary.

There should only ever be one bean implementation marked as @Primary for a given dependency.

Example

// Highest priority EmailServer
// Used when available (e.g. module in the class path)
@Primary
@Singleton
public class PreferredEmailSender implements EmailServer {
  ...

@Secondary

A bean with @Secondary is deemed to be lowest priority and will only be injected if there are no other candidates to inject. We use @Secondary to indicate a "default" or "fallback" implementation that will be superseded by any other available implementation.

This is functionally the same as Spring and Micronaut @Secondary.

Example

// Lowest priority EmailServer
// Only used if no other EmailServer is available
@Secondary
@Singleton
public class DefaultEmailSender implements EmailServer {
  ...

Use of @Primary and @Secondary

@Primary and @Secondary are used when there are multiple candidates to inject. They provide a "priority" to determine which dependency to inject and use when injecting a single implementation and multiple candidates are available to inject.

We typically use @Primary and @Secondary when we are building multi-module applications. We have multiple modules (jars) that provide implementations. We use @Secondary to indicate a "default" or "fallback" implementation to use and we use @Primary to indicate the best implementation to use when it is available. avaje-inject DI will then wire depending on which modules (jars) are included in the classpath.

Qualifiers

@Named

When we have multiple beans that implement a common interface we can qualify which instance is used by specifying @Named on the beans and where they are injected. This is a standard part of most Java DI frameworks.

Note that qualifier names are treated as case insensitive.

Lets say we have a Store interface with multiple implementations. We can have multiple implementations with @Named qualifier like the example below.

@Named("blue")
@Singleton
public class BlueStore implements Store {
  ...
}


@Named("red")
@Singleton
public class RedStore implements Store {
  ...
}

Alternatively if we are creating the instances using @Factory @Bean methods we can similarly put @Named on the @Bean methods.

@Factory
public class StoreFactory {

  @Bean @Named("red")
  public Store createRedStore() {
    return new RedStore(...);
  }

  @Bean @Named("blue")
  public Store createBlueStore() {
    return new BlueStore(...);
  }
}

Finally, we can specify the name when explicitly registering a bean with a BeanScope.

We can then specify which @Named instance to inject by specifying the qualifier.

@Singleton
public class OrderProcessor {
  private final Store store;

  public OrderProcessor(@Named("red") Store store) {
    this.store = store;
  }
  ...
@Singleton
public class OrderProcessor {

  @Inject @Named("red")  // field injection
  Store store;

  ...

@Named is a standard part of Java dependency injection frameworks. Avaje Inject goes further and eliminates the need for writing out the annotation at all. All injectable parameters or fields that don't specify @Named explicitly are implicitly given a name of !name. So the above can be more cleanly written by relying on this implicit rule, like this:

@Singleton
public class OrderProcessor {
  private final Store store;

  public OrderProcessor(Store red) {
    this.store = store;
  }
  ...

This type of implicit naming is useful if you want to inject things with a relatively widely used type, for example, a java.nio.file.Path object.

@Qualifier

Instead of using @Named we can create our own annotations using @Qualifier. This gives a strongly typed approach to qualifying the beans rather than using string literals in @Named so could be better when there is a lot of named/qualified beans.

example
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Blue {}

Then we can use our @Blue annotation.

@Blue
@Singleton
public class BlueStore implements Store {
  ...
@Singleton
public class StoreManager {

  private final Store store;

  public StoreManager(@Blue Store store) {
    this.store = store;
  }
  ...

Lifecycle

@PostConstruct

Put @PostConstruct on a method that we want to run on startup just after all the beans have been wired.

Typically we open a resource like network connections to a remote resource (cache, queue, database etc).

@Singleton
public class CoffeeMaker {

  @PostConstruct
  void onStartup() {
    // connect to remote resource ...
    ...
  }
  ...

@PreDestroy

Put @PreDestroy on a method that we want to run on shutdown.

Typically we want this method to close resources.

@Singleton
public class CoffeeMaker {

  @PreDestroy
  void onShutdown() {
    // close resources
    ...
  }
  ...

AutoCloseable and Closeable

Both java.lang.AutoCloseable and java.io.Closeable are treated as PreDestroy lifecycle methods. Types that implement these interfaces do not need to annotate the close() method, it will automatically be treated as if it had a @PreDestroy and executed when the bean scope is closed.

@Singleton
public class CoffeeQueue implements AutoCloseable {

  /**
   * Automatically treated as a PreDestroy method.
   */
  @Override
  public void close() {
    // close resources
    ...
  }
  ...

Shutdown hook

When BeanScope is created we can specify if it should register a JVM shutdown hook. This is fired when the JVM is shutdown and this in turn invokes the PreDestroy methods. Otherwise PreDestroy methods are closed when the BeanScope is closed.

BeanScope beanScope =
  BeanScope.builder()
      .shutdownHook(true) // create with JVM shutdown hook
      .build()

Default scope

All beans are instantiated within a scope. Beans annotated with @Singleton are in the "default scope".

public class App {
  public static void main(String[] args) {

    // create all the beans in the "default scope"
    BeanScope scope = BeanScope.builder().build();

    SomeObject obj = scope.get(SomeObject.class);
  }
}

When avaje-inject builds the "default scope" it will service load all the default scope modules in the classpath (i.e. wire all the "default scope" modules in the classpath together into the BeanScope).

Test scope

Test scope is a special scope used for testing. It effectively provides default dependencies to use for all tests.

Refer to Testing - Test Scope for more details.

Request scope - @Controller

When using avaje-http we annotate controllers with @Controller. avaje-inject will detect when controllers have a request scope dependency and automatically make them request scoped.

For the following example, the ContactController has a dependency on Javalin Context. This means this controller must use request scope.

// Automatically becomes request scoped
//  ... because Javalin Context is a dependency
//  ... controller instantiated per request

@Controller
@Path("/contacts")
class ContactController {

  private final ContactService contactService;

  private final Context context; // Javalin Context

  // Inject Javalin context via constructor
  @Inject
  ContactController(Context context, ContactService contactService) {
    this.context = context;
    this.contactService = contactService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    // use the javalin context ...
    var fooCookie = context.cookieStore("foo");
    ...
  }
}

@Scope - custom scopes

We can define our own custom scopes. To do this we create an annotation that is meta-annotated with @Scope. We use this custom scope annotation rather than @Singleton.

Custom scopes can depend on each other and also externally defined objects, thus allowing a hierarchy of scopes to be modelled.

Each scope results in the generation of a module class that can be added to a BeanScope using the modules method. The constructor of the module class takes the externally defined dependencies if defined. This makes it easy to partially adopt Avaje Inject where we want some objects can be built manually by your own code and then provided to the DI scope.

For simple cases we don't need to use custom scopes. We can just annotate classes with @Singleton and use the default scope.

Example: Custom scope

Step 1: Define the custom scope annotation
@Scope
public @interface MyCustomScope {
}
Step 2: Use the custom scope annotation (rather than @Singleton)
@MyCustomScope
public class SomeObject {

}
Step 3: Build BeanScope with the custom scope
public class App {
  public static void main(String[] args) {

    BeanScope scope = BeanScope.builder()
      .modules(new MyCustomScopeModule())
      .build();

    SomeObject obj = scope.get(SomeObject.class);
  }
}

Custom scope dependencies

Custom scopes can have dependencies on other scopes or externally supplied beans. We specify these dependencies using @InjectModule(requires = ...).

Note that custom scope beans are allowed to depend on any bean in the "default scope" implicitly. We do not need to specify a dependency for custom scoped beans to use default scoped beans.

In the following example MyCustomScope has a dependency on NonDIConstructedObject.

@Scope
@InjectModule(requires = {NonDIConstructedObject.class})
public @interface MyCustomScope {
}

@MyCustomScope
public class SomeObject {
  public SomeObject(NonDIConstructedObject obj) { ... }
}

public class App {
  public static void main(String[] args) {

    BeanScope scope = BeanScope.builder()
      // custom module with an external dependency
      .modules(new MyCustomScopeModule(new NonDIConstructedObject()))
      .build();

    SomeObject obj = scope.get(SomeObject.class);
  }
}

To make one scope depend on another, just put the depended-on scope's annotation into the @InjectModule(requires = { .. }) list, then call the parent method of the BeanScope to chain them together.

Custom scope parent child hierarchy

When using custom scopes we can create a hierarchy of scopes. When we create the BeanScope we can use parent() to specify a parent scope.

// create a parent scope
try (final BeanScope parentScope = BeanScope.builder().build()) {

  // we can use this scope
  final var coffeeMaker = parentScope.get(CoffeeMaker.class);

  // external dependency for a custom scope
  LocalExt ext = new LocalExt();

  // create a child scope
  try (BeanScope childScope = BeanScope.builder()
    .parent(parentScope) // specify the parent
    .modules(new MyCustomModule(ext)) // the custom scope(s)
    .build()) {

    // use the child scope
    ...

  }
}

BeanScope

The methods on BeanScope that we use to obtain beans out of the scope are:

get(type)

Return a single bean given the type.

StoreManager storeManager = beanScope.get(StoreManager.class);
StoreManager.processOrders();
get(type, qualifier)

Return a single bean given the type and qualifier name.

Store blueStore = beanScope.get(Store.class, "blue");
blueStore.checkOrders();
list(type)

Return the list of beans that implement the interface.

// e.g. register all routes for a web framework

List<WebRoute> routes = beanScope.list(WebRoute.class);
listByAnnotation(annotation type)

Return the list of beans that have an annotation.

// e.g. register all controllers with web a framework
// .. where Controller is an annotation on the beans

List<Object> controllers = beanScope.listByAnnotation(Controller.class);

The classic use case for this is registering controllers or routes to web frameworks like Sparkjava, Javalin, Rapidoid, Helidon, Undertow etc.

listByPriority(type)

Return the list of beans that implement the interface sorting by priority.

// e.g. filters that should be applied in @Priority order

List<Filter> filters = beanScope.listByPriority(Filter.class);

BeanScope.builder()

We can programmatically create a BeanScope with the option on providing some instances to use as dependencies. Most often we will do this for component testing providing mocks, spies etc to be wired into the beans.

// provide dependencies to be wired
// ... can be real things or test doubles
MyDependency dependency = ...

try (BeanScope scope = BeanScope.builder()
  .bean(MyDependency.class, dependency)
  ...
  .build()) {

  CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
  coffeeMaker.makeIt();
}

See Testing for more on wiring with test doubles.

Testing

Unit testing

When we are unit testing we are not using avaje-inject. We are focused on the thing we want to test (object under test) and it's dependencies.

With unit testing in the test setup code will create the thing we are testing (object under test) along with it's dependencies.

Mockito programmatic style
// setup
final Pump pump = mock(Pump.class);
final Grinder grinder = mock(Grinder.class);
CoffeeMaker coffeeMaker = new CoffeeMaker(pump, grinder);

// act
coffeeMaker.makeIt();

verify(pump).pumpSteam();
verify(grinder).grindBeans();
Mockito junit5 extension

Mockito provides a JUnit 5 extension MockitoExtension which can be used with JUnit @ExtendWith. With this extension we can annotate fields with @Mock, @Spy and @Captor. Again, this is all Mockito - no avaje inject is used here.

@ExtendWith(MockitoExtension.class)
class CoffeeMakerTest {

  @Mock Pump pump;
  @Mock Grinder grinder;

  @Test
  void extensionStyle() {

    // setup
    CoffeeMaker coffeeMaker = new CoffeeMaker(pump, grinder);

    // act
    coffeeMaker.makeIt();

    verify(pump).pumpSteam();
    verify(grinder).grindBeans();
  }
}

avaje-inject is NOT used in the above unit tests (as expected). We will see below that avaje-inject provides a JUnit extension similar to the Mockito one and that uses the Mockito annotations @Mock, @Spy, @Captor and also adds @Inject.

Component testing

Component testing is where we look to run tests that use most of the objects with their real behaviour with less mocked / stubbed behaviour. With component testing we are looking to test a scenario / piece of functionality with minimal to no mocking.

The rise and adoption of test docker containers has meant that it is now possible to test significant portions of an application without mocking or stubbing resources like databases and messaging.

dependency

Add avaje-inject-test as a test dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject-test</artifactId>
  <version>8.8</version>
  <scope>test</scope>
</dependency>

@InjectTest

avaje-inject provides a JUnit 5 extension via @InjectTest. When a test is annotated with @InjectTest then avaje-inject will be used to setup the test using @Inject as well as mockito's @Mock, @Spy, @Captor.

With @InjectTest avaje-inject will build a BeanScope will the appropriate mockito mocks and spies and inject back into the test the appropriate objects out of the BeanScope.

@InjectTest
class CoffeeMakerTest {

  @Mock Pump pump;
  @Mock Grinder grinder;
  @Inject CoffeeMaker coffeeMaker;

  @Test
  void extensionStyle() {

    // act
    coffeeMaker.makeIt();

    verify(pump).pumpSteam();
    verify(grinder).grindBeans();
  }
}

The above test using @InjectTest is equivalent to the test below that programmatically creates a BeanScope and performs the same test.

@Test
void programmaticStyle() {

  try (var beanScope = TestBeanScope.builder()
    .forTesting()
    .mock(Pump.class)
    .mock(Grinder.class)
    .build()) {

    CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class);

    // act
    coffeeMaker.makeIt();

    verify(beanScope.get(Pump.class)).pumpSteam();
    verify(beanScope.get(Grinder.class)).grindBeans();
}

If we look closely at the test above you will see the use of TestBeanScope.builder() rather than the usual BeanScope.builder(). We use TestBeanScope to automatically use the "test scope" if it exists.

@Named and @Qualifier

We can use @Named and qualifiers as needed with @Mock, @Spy, and @Inject like below.

@Mock @Blue Store blueStore;

@Mock @Named("red") Store redStore;

Static fields, Instance fields

With @InjectTest we can inject into static fields and non-static fields. Under the hood these map to BeanScopes that are created and used to populate these fields in the tests.

static fields - Junit All

With static fields there is an underlying BeanScope that is created and used for all tests in the test class. In the example below, the static Foo is in that BeanScope.

This matches Junit5 All - @BeforeAll, @AfterAll etc.

This BeanScope is created and used for all tests that run for that test class. This scope will be closed after all tests for the class have been run. The "global test scope" (if defined) will be the parent bean scope.

non-static fields - Junit Each

For non-static fields, there is a BeanScope that is created for these. In the example below Bar and Bazz are in that BeanScope. With the 3 test methods one(), two(), three() this BeanScope is created (and closed) for each test so 3 times.

This matches Junit5 Each - @BeforeEach, @AfterEach etc.

This BeanScope is created for each test and closed after that test has been run. It can have a parent bean scope of either the "test static scope" if defined for that test, or the "global test scope" (if defined).

@InjectTest
class MyTest {

  static @Mock Foo foo;
  @Mock Bar bar;
  @Inject Bazz bazz;

  @Test
  void one() {
    ...
  }

  @Test
  void two() {
    ...
  }

  @Test
  void three() {
    ...
  }

}

The above MyTest runs 3 tests, one(), two(), and three().

Static Foo field is wired once and the same foo instance would be used for ALL three tests.

Instance Bar, Bazz fields are wired for each of the three tests - they are wired 3 times.

This can be represented by the diagram below. Bar, Bazz are created and injected 3 times. Static Foo is created and injected once.

 

The above test can be programmatically written as below. In the code below, we see:

class MyTest {

  // static fields
  static @Mock Foo foo;

  // instance fields
  @Mock Bar bar;
  @Inject Bazz bazz;

  static BeanScope staticScope; // "static forAll" scope
  BeanScope instanceScope    // "instance forEach" scope

  @BeforeAll
  static void beforeAll() {
    staticScope = TestBeanScope.builder()
        .forTesting()
        .mock(Foo.class)
        .build()
    foo = staticScope.get(Foo.class);
  }

  @AfterAll
  static void afterAll() {
    staticScope.close()
  }

  @BeforeEach
  void beforeEach() {
    instanceScope = TestBeanScope.builder().parent(staticScope)
        .forTesting()
        .mock(Bar.class)
        .build()
    bar = instanceScope.get(Bar.class);
    bazz = instanceScope.get(Bazz.class);
  }

  @AfterEach
  void afterEach() {
    instanceScope.close()
  }


  @Test
  void one() {
    ...
  }

  @Test
  void two() {
    ...
  }

  @Test
  void three() {
    ...
  }
}

Parent child hierarchy

When using @InjectTest we get a 3 level parent child hierarchy of BeanScope.

  1. Global test scope that spans ALL tests. This is detailed in the next section.
  2. Static/All BeanScope - when there are static fields to @Inject, @Mock or @Spy.
  3. Instance/Each BeanScope - when there are instance fields to @Inject, @Mock or @Spy.

All 3 scopes

When we have all 3 scopes they form a parent child hierarchy as per the diagram below. The "global test scope" is the parent of the "static/all scope". The "static/all scope" is the parent of each "instance/each scope".

 

Only instance fields

When a test only has instance fields with @Inject, @Mock or @Spy then the global test scope (if defined) is the parent of the instance/each scopes.

In this case, @InjectTest detects that there are no static fields to wire and will not create a BeanScope for the static/all scope.

 

Only static fields

When a test only has static fields with @Inject, @Mock or @Spy then the global test scope (if defined) is the parent of the static/all scope. This static/all scope is used by all the tests that run.

In this case, @InjectTest detects that there are no instance fields to wire and will not create a BeanScope for each instance/each scope.

 

@TestScope - global test scope

When we use @TestScope we create a special bean scope used for testing that spans ALL TESTS. It effectively provides default dependencies to use for ALL tests. As such we can think of it as the "global test scope".

Under the hood, the global test BeanScope is created when junit starts, this test BeanScope holds all the beans that we put in @TestScope and this scope is used as the parent BeanScope for @InjectTest tests.

Step 1: Add @TestScope

In src/test create a factory bean that we dedicate to creating test scope dependencies. Put @TestScope on this factory bean, the beans this factory creates are in our "test scope" and will be wired into tests that use @InjectTest.

Example - AmazonDynamoDB

In the example below, our application has a dependency on AmazonDynamoDB. For testing purposes, we want all tests to default to using a test scoped AmazonDynamoDB instance that we set up to use a localstack docker container.

Example: avaje-inject-examples - hello-dynamodb - MyTestConfiguration.java

/**
 * All beans wired by this factory will be in the "global test scope".
 */
@TestScope
@Factory
class MyTestConfiguration {

  /**
   * An 'extra' dependency for testing - a docker container running DynamoDB.
   */
  @Bean
  LocalstackContainer dynamoDBContainer() {
    LocalstackContainer container = LocalstackContainer
      .builder("0.14.2")
      .services("dynamodb") // e.g. "dynamodb,sns,sqs"
      .build();
    container.start();
    return container;
  }

  /**
   * Default to using this AmazonDynamoDB instance in our tests.
   * This client is setup to use the localstack docker container.
   */
  @Bean
  AmazonDynamoDB dynamoDB(LocalstackContainer container) {
    AmazonDynamoDB dynamoDB = container.dynamoDB();
    createTable(dynamoDB);
    return dynamoDB;
  }
}

Any component that has AmazonDynamoDB injected into it, will now have the above AmazonDynamoDB instance which is set up to talk to the localstack docker container DynamoDB.

Step 2: @InjectTest

Annotation the test class with @InjectTest.

The component tests can inject AmazonDynamoDB directly, or typically inject a component that depends on AmazonDynamoDB and these will use "our test scoped AmazonDynamoDB instance".

@InjectTest
class DynamoDbComponentTest {

  /**
   * The test scoped instance.
   */
  @Inject AmazonDynamoDB dynamo;

  /**
   * More typical, a component that depends on AmazonDynamoDB.
   */
  @Inject MyDynamoClient client;

}

The "test scoped bean" is by default wired but each test can override this using @Mock or @Spy. For that test, the mock or spy is used and wired instead of the "test scoped bean".

@InjectTest
class OtherComponentTest {

  /**
   * Use this instance for this test.
   */
  @Mock AmazonDynamoDB mockDynamo;

  /**
   * Now wired with mockDynamo for this test.
   */
  @Inject MyDynamoClient client;

}

Purposes of Test scope beans

Test scope beans generally have one of 3 purposes.

1. Extra bean

For testing purposes we want to create an extra bean. For example, the LocalstackContainer that starts a docker container.

2. Default bean

We want most of the tests to use a bean (the default one we want to use in tests). For example, we want components to use the AmazonDynamoDB instance that will talk to the local docker container.

3. Replacement

Say we have a remote API (e.g. Rest call to Github). We don't want any component tests to actually make real calls to Github. Instead, we want to have a default stub response and have that as the default. This is similar to (2) but more that the default is more like a stub test double.

Note that @InjectTest is syntactic sugar for junit5 @ExtendWith(InjectExtension.class)

Programmatic testing

As an alternative to using InjectExtension, we can write component tests programmatically. For programmatic style component testing we create a BeanScope and define test doubles that we want to use in place of the real things.

TestBeanScope.builder()

For tests we should always use TestBeanScope.builder() rather than BeanScope.builder().

By using TestBeanScope.builder() it will use the global test scope as a parent bean scope - we generally always want to do that for tests.

If we never use the global test scope then we can use BeanScope.builder().

forTesting()

With the bean scope builder we use forTesting() to give us extra methods for ease of using mockito mocks and spies. Use mock() to specify a dependency to be a Mockito mock. Use spy() to get a dependency to be a Mockito spy. We can use bean() to supply any sort of test double we like.

@Test
void using_mock() {

  try (BeanScope scope = TestBeanScope.builder()
    .forTesting()
    .mock(Pump.class)
    .mock(Grinder.class)
    .build()) {

    // act
    CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
    coffeeMaker.makeIt();

    verify(pump).pumpSteam();
    verify(grinder).grindBeans();
}
@Test
void using_spy() {

  try (BeanScope context = TestBeanScope.builder()
    .forTesting()
    .spy(Pump.class)
    .build()) {

    CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
    assertThat(coffeeMaker).isNotNull();
    coffeeMaker.makeIt();

    Pump pump = context.get(Pump.class);
    verify(pump).pumpWater();
  }
}

Modules

Unnamed modules

When we don't specify @InjectModule on a module it is an unnamed module. In effect a module name is derived from the top most package and that module has no provides or requires specified.

For example, if the top level package is determined to be org.foo.coffee then the module is called CoffeeModule. In target/generated-sources/annotations we will see the generated org.foo.coffee.CoffeeModule.

Single module apps

When we are wiring dependencies that are all part of a single jar/module then we don't really care about module ordering. All the dependencies that are being injected are known and provided by the same jar/module or provided externally.

requires - external dependency

We use @InjectModule(requires = ...) to specify an external dependency that will be programmatically provided.

// at compile time, allow injection of MyExternalDependency
// ... even though it doesn't exist in this module
@InjectModule(requires = MyExternalDependency.class)

When compiling the annotation processor checks that dependencies exist or are listed in requires (or requiresPackages) meaning that they are externally supplied dependencies.

When creating the BeanScope we provide the externally created dependencies using bean(). These external dependencies are then injected where needed.

MyExternalDependency myExternal = ...;

// create with an externally provided dependency
final BeanScope scope = BeanScope.builder()
  .bean(MyExternalDependency.class, myExternal)
  .build();

ignoreSingleton

We use @InjectModule(ignoreSingleton = true) in order to specify that we want avaje-inject to ignore standard JSR-330 @Singleton - with the expectation that another DI library (like Guice) is being used and we want avaje-inject to co-exist and ignore anything annotated with @Singleton.

@InjectModule(ignoreSingleton = true)

When using ignoreSingleton = true we use @Component instead of using @Singleton.

Multi-module apps

When we are wiring dependencies that span multiple jars/modules then we to provide more control over the order in which the modules are wired. We provide this control by using @InjectModule and the use of provides, and requires or requiresPackages.

Example - modular coffee app

See example source at avaje-inject-examples / modular-coffee-app

module 1 - coffee-heater
@InjectModule(name="coffee-heater", provides = Heater.class)
module 2 - coffee-pump
@InjectModule(name = "coffee-pump", requires = Heater.class, provides = Pump.class)

This module expects Heater to be provided by another module. In effect, the coffee-heater module must be wired before this module, and it provides the Heater that is required when we wire the coffee-pump module.

module 3 - coffee-main
@InjectModule(name = "coffee-main", requires = {Heater.class, Pump.class})

This module expects Heater and Pump to be provided by other module(s). It needs other modules to be wired before and for those modules to provide the Heater and Pump.

avaje-inject determines the order in which to wire the modules based on provides, requires. In this example it needs to wire the modules in order of: coffee-heater, coffee-pump and then coffee-main.

That is, in a multi-module app avaje-inject creates one BeanScope per module and needs to determine the order in which the modules are wired. It does this using the module provides and requires. As the modules are wired, the beans from any previously wired modules are available to the module being wired.

requires vs requiresPackages

We use either requires OR requiresPackages. Using requires is nice in that we explicitly list each external dependency that the module requires BUT this can get onerous when this is a really large list. In this case we can use requiresPackages instead and that makes the assumption that any dependency under those packages will be provided. So using requiresPackages is less strict but more practical when there is a lot of module dependencies.

When we use requiresPackages that means that provides can similarly specify a class at the top level, and we don't need to list ALL the provided dependencies.

@InjectModule

name

Give the module an explicit name. Otherwise, it is derived from the top level package.

ignoreSingleton

Set this to true in order to specify that we want avaje-inject to ignore standard JSR-330 @Singleton with the expectation that another DI library (like Guice) is being used and we want avaje-inject to co-exist and ignore anything annotated with @Singleton.

provides

List the classes that this module provides. Used to order modules.

@InjectModule(name = "feature-toggle", provides=FeatureToggle.class)

requires

Defines dependencies that the modules depends on that are provided by another module or manually.

@InjectModule(name = "job-system", requires=FeatureToggle.class)

In effect this allows the job system components to depend on FeatureToggle with the expectation that it will be supplied by another module or supplied manually.

requiresPackages

If we have a LOT of dependencies provided by another module specifying each of these explicitly in requires can get verbose. Instead of doing that we can use requiresPackages to define top level packages, any dependency required under top level packages will be expected to be provided by another module.

// anything at or below the package of Feature is provided externally
@InjectModule(name = "job-system", requiresPackages=Feature.class)

avaje-inject uses provides, requires, requiresPackages to determine the order in which the modules are created and wired. avaje-inject finds all the modules in the classpath (via Service loader) and then orders the modules based on provides, requires, requiresPackages. In the example above the "feature-toggle" module must be wired first, and then the beans it contains are available when wiring the "job-system".

Why avaje inject exists

Short History of DI on the JVM

For a short history of DI on the JVM see below and refer to PicoContainer - inversion of control history

Stefano Mazzocchi popularises the term, Inversion of Control and the Apache Avalon project starts.

Spring and PicoContainer lead the initial adoption of DI. Java 5 came out with new language features which included annotations and generics which lead to the creation of Guice.

In 2009 developers from Spring, Guice, Redhat and some others got together to define JSR-330 - dependency injection for Java.

Some Guice developers went on to develop Dagger which I believe was the first dependency injection library that used Java annotation processing to generate code for DI. This moves work that was previously done at runtime to build time and made Dagger significantly faster and lighter than Guice. Dagger becomes heavily adopted for Android applications.

Using Java annotation processing to move dependency injection work from run time to build time is common approach by Dagger, Micronaut, avaje-inject and Quarkus.

Around 2018 the pain points of using Spring DI with Kubernetes and resource limited containers becomes more obvious. The way Spring DI works means that at startup time it performs a lot of work that includes classpath scanning, heavy use of reflection and defining dynamic proxies. This makes it relatively slow and hungry for both CPU and memory resources. With cloud deployment where we pay for CPU and memory some developers start looking for another approach to DI which puts more (or virtually all) of that work to build time using Java annotation processing [as Dagger2 had been doing in the Android space for some years].

In 2018 Micronaut and avaje-inject (this library) are released which use Java annotation processing to perform most of DI at build time rather than runtime. In 2019 Quarkus is released which similarly uses Java annotation processing but is based on CDI.

Quick comparison to other DI libraries

Why not use Dagger2?

Dagger2 is not particularly orientated for server side developers with no lifecycle support (@PostConstruct + @PreDestroy) and does not have some features that we like from Spring DI (@Factory + @Bean, @Primary + @Secondary).

Why not use Quarkus?

Quarkus comes with a DI implementation based on CDI. If CDI is your thing you'd look at Quarkus.

Why not Micronaut?

Micronaut DI and avaje-inject are both heavily influenced by Spring DI and to some extent look pretty similar. avaje-inject has taken a source code generation approach, excluded AOP and excluded @Value with a preference instead to use a simple avaje-config style approach to external configuration.

Ultimately avaje-inject is a relatively tiny library in comparison to Micronaut DI and focused on DI alone.

http servers

avaje-inject is a lightweight DI library that is especially suited to building http based services using Javalin and Helidon SE.

We can build rest controllers and target either Javalin or Helidon SE using as little or as much of either Javalin or Helidon as we like.

Javalin

Create JAX-RS style controllers targeting Javalin. Write controller methods that take the Javalin context as a method argument or have it injected as a request scoped bean.

See here for more details.

Helidon SE

Create JAX-RS style controllers targeting Helidon SE. Write controller methods that take the Helidon server request and/or response as method arguments or have them injected into the controller as request scoped beans.

See here for more details.

Spring DI

@Factory + @Bean

avaje-inject has @Factory + @Bean which work the same as Spring DI's @Configuration + @Bean and also Micronaut's @Factory + @Bean.

@Primary @Secondary

avaje-inject has @Primary and @Secondary annotations which work the same as Spring DI's @Primary @Secondary and also Micronaut DI's @Primary @Secondary.

These provide injection precedence in the case of injecting an implementation when multiple injection candidates are available.

 

Spring DI translation

Spring avaje-inject JSR-330
@Component, @Service, @Repository @Singleton Yes
FactoryBean<T> Provider<T> Yes
@Inject, @Autowired @Inject Yes
@Autowired(required=false) @Inject Optional<T> -
@PostConstruct @PostConstruct JSR 250
@PreDestroy @PreDestroy JSR 250
Non standard extensions to JSR-330
@Configuration @Bean @Factory @Bean No
@Primary @Primary No
@Secondary @Secondary No

 

 

Spring DI translation NOT part of avaje-inject

Spring Other
@Value avaje-config
@Controller avaje-http @Controller
@Transactional Ebean @Transactional