Avaje Inject

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

Discord Source API Docs Issues Releases
Discord Github Javadoc Github

 

Inject leverages Java annotation processing to generate source code for efficient dependency injection. By shifting the responsibility of dependency injection from runtime to build time, it significantly enhances the speed of application startup. This approach eliminates the need for intensive reflection or classpath scanning, further optimizing performance.

The dependency injection classes are generated source code. This allows us to seamlessly incorporate debug breakpoints into the DI code, enabling us to step through the code as if it were manually written. We can use existing IDE tools to search where code is called (e.g. Constructors and lifecycle methods.)

For a background on why avaje inject exists and a quick comparison with other DI libraries such as Dagger2, Micronaut, Quarkus and Spring go to - 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 should 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 jakarta.inject?

Use version 9.x of avaje-inject with the dependency on jakarta.inject

Want to use javax.inject?

Use version 9.x-javax of avaje-inject with the dependency on 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 were in JDK 8, but from JDK 9 onwards are part of JDK module javax.annotation-api.

Currently, neither Dagger2 or Guice support or plan to support @PostConstruct and @PreDestroy lifecycle annotations.

DI extensions to JSR-330

Component Testing

We use @InjectTest for component testing, similar to Spring's @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 have a similar function 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.

Quick Start

1. Add avaje-inject as a dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject</artifactId>
  <version>${avaje.inject.version}</version>
</dependency>

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

<!-- Annotation processors -->
<!-- if using lombok, it must be placed before the inject generator.
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency> -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject-generator</artifactId>
  <version>${avaje.inject.version}</version>
  <scope>provided</scope>
  <optional>true</optional>
</dependency>

3. Create a Bean Class annotated with @Singleton

@Singleton
public class Example {

 private DependencyClass d1;
 private DependencyClass2 d2;

  // Dependencies must be annotated with singleton,
  // or else be provided from another class annotated with @Factory
  public Example(DependencyClass d1, DependencyClass2 d2) {
    this.d1 = d1;
    this.d2 = d2;
  }
}

Example factory class:

@Factory
public class ExampleFactory {
  @Bean
  public DependencyClass2 bean() {
    return new DependencyClass2();
  }
}

4. Use BeanScope to wire and retrieve the beans and use however you wish..

BeanScope beanScope = BeanScope.builder().build()
Example ex = beanScope.get(Example.class);

Dependencies

Maven

See the quick start example

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:${inject.version}')
  annotationProcessor('io.avaje:avaje-inject-generator:${inject.version}')

  testImplementation('io.avaje:avaje-inject-test:${inject.version}')
  testAnnotationProcessor('io.avaje:avaje-inject-generator:${inject.version}')
}

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:${inject.version}')
  kapt('io.avaje:avaje-inject-generator:${inject.version}')

  testImplementation('io.avaje:avaje-inject-test:${inject.version}')
}

Java Module Setup

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
import io.avaje.inject.spi.Module;

module org.example {

  requires io.avaje.inject;
  // you must define the fully qualified class name of the generated classes. if you use an import statement, compilation will fail
  provides Module with org.example.ExampleModule;
}

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

External Avaje Dependencies

If your project uses the module system and imports maven dependencies that provide inject Plugin/Module classes, you will need to add the maven/gradle inject plugin so that the generated DI classes from the dependencies are discovered.

Generated Sources

DI classes

DI classes will be generated to call the constructors for annotated type/factory methods.

Below is the class generated for the Example class in the above quickstart.

@Generated("io.avaje.inject.generator")
public final class Example$DI  {

  /**
   * Create and register Example.
   */
  public static void build(Builder builder) {
    if (builder.isAddBeanFor(Example.class)) {
      var bean = new Example(builder.get(DependencyClass.class,"!d1"), builder.get(DependencyClass2.class,"!d2"));
      builder.register(bean);
      // depending on the type of bean, callbacks for field/method injection, and lifecycle support will be generated here as well.
    }
  }
}

Generated Wiring Class

The inject annotation processor will determine the dependency wiring order of a project and generate a Module class that will wire the beans.

Generated ExampleModule
@Generated("io.avaje.inject.generator")
@InjectModule
public final class ExampleModule implements Module {

  private Builder builder;

  @Override
  public Class<?>[] classes() {
    return new Class<?>[] {
      org.example.DependencyClass.class,
      org.example.DependencyClass2.class,
      org.example.Example.class,
      org.example.ExampleFactory.class,
    };
  }

  /**
   * Creates all the beans in order based on constructor dependencies. The beans are registered
   * into the builder along with callbacks for field/method injection, and lifecycle
   * support.
   */
  @Override
  public void build(Builder builder) {
    this.builder = builder;
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_example_ExampleFactory();
    build_example_DependencyClass();
    build_example_DependencyClass2();
    build_example_Example();
  }

  @DependencyMeta(type = "org.example.ExampleFactory")
  private void build_example_ExampleFactory() {
    ExampleFactory$DI.build(builder);
  }

  @DependencyMeta(type = "org.example.DependencyClass")
  private void build_example_DependencyClass() {
    DependencyClass$DI.build(builder);
  }

  @DependencyMeta(
      type = "org.example.DependencyClass2",
      method = "org.example.ExampleFactory$DI.build_bean", // factory method
      dependsOn = {"org.example.ExampleFactory"}) //factory beans naturally depend on the factory
  private void build_example_DependencyClass2() {
    ExampleFactory$DI.build_bean(builder);
  }

  @DependencyMeta(
      type = "org.example.Example",
      dependsOn = {"org.example.DependencyClass", "org.example.DependencyClass2"})
  private void build_example_Example() {
    Example$DI.build(builder);
  }
}

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 {
  ...

@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.

@Component.Import

Put @Component.Import on a class/package-info to create dependency injection on external classes (e.g. mvn dependencies). It has the same effect as if the bean was directly annotated by @Component.

@Component.Import(TeaMaker.class)
@Singleton
public class CoffeeMaker {
  ...

@Inject

Put @Inject on the constructor that should be used for constructor dependency injection.

If we want to use field injection put the @Inject on the field. 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.

Single Constructors

If there is only one constructor, we don't need to specify @Inject. This includes records and kotlin data classes.

@Singleton
public class CoffeeMaker {

  private final Pump pump;
  private final Grinder grinder;

  public CoffeeMaker(Pump pump, Grinder grinder) {
    this.pump = pump;
    this.grinder = grinder;
  }
}
@Singleton
public record CoffeeMaker(Pump pump, Grinder grinder) {}
@Singleton
class CoffeeMaker(private val pump: Pump, private val grinder: Grinder) {}

Field Injection

@Singleton
public class CoffeeMaker {

  @Inject
  Pump pump;

  @Inject
  Grinder grinder;
  ...

With field injection the @Inject is placed on the field. The field cannot be private or 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 a 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 cannot 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 cannot use constructor injection for both A and B, instead we must use either field/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/method injection occur after all the dependencies are constructed. avaje-inject uses 3 phases to construct a bean scope:

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/jakarta).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.

An alternative to implementing the Provider<T> interface is to instead use @Factory and @Bean as can be more flexible and convenient.

@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 autocloseable

The avaje annotation processor reads the bean method return types to detect if the bean is an instance of Closeable or AutoCloseable. In the case where you are wiring an interface that doesn't implement these types, but the concrete class implements, we can specify autocloseable to inform the processor.

@Factory
class Configuration {
  ...
  @Bean(autocloseable = true)
  CoffeeMaker buildCoffeeMaker(Pump pump) {
    return new CloseableCoffeeMaker(pump);
  }
}

@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.

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.

@Prototype

When the @Prototype annotation is added to a class/factory bean method, a new instance of the bean will be created each time it is requested or wired.

@Prototype
public class Proto {
  //every time this bean is requested, the constructor is called to provide another instance
  ...
  }
@Factory
class ProtoFactory {
  //every time this bean is requested, the factory method is called to provide another instance
  @Bean
  @Prototype
  Example proto() {
    return new Example();
  }

}

@Lazy

We can use @Lazy on beans/factory methods to defer bean initialization until the annotated bean is requested.

@Lazy
public class Sloth {

  private final Moss moss;

  @Inject //this will not be called until the bean is requested
  Sloth(Moss moss) {
    this.moss = moss;
  }
}

There are two ways to lazily load the bean.

1. Directly retrieve from the bean scope:

final var scope = BeanScope.builder().build();
scope.get(Sloth.class); //Sloth is initialized here

2. Use a Provider

Unlike regular provider beans, the providers for lazy beans will return the same singleton instance.

@Singleton
class UseSloth  {

  private final Provider<Sloth> slothProvider;

  UseFoo(Provider<Sloth> slothProvider) {
    this.slothProvider = slothProvider;
  }

  void doStuff() {

    // get the singleton Sloth instance and use it
    Sloth sloth = slothProvider.get();
    ...
  }
}

Qualifiers

@Named

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

Qualifier names are case insensitive.

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

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


@Singleton
@Named("red")
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 jakarta.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;
  }
  ...

Qualifiers with members

Java annotations can have members. We can use these members to further discriminate a qualifier. This prevents a potential explosion of new annotation interfaces. For example, instead of creating several qualifiers representing different payment methods, we could aggregate them into a single annotation with a member:

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Accepts {
   CardType value();
}

Then we select one of the possible member values when applying the qualifier:

@Accepts(VISA)
@Singleton
public class VisaStore implements Store {
  ...
@Singleton
public class StoreManager {

  private final Store store;

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

@QualifiedMap

To receive a map of beans keyed by qualifier name, we can use @QualifiedMap.

@Singleton
 class CrewMate {

   private final Map<String, Tasks> taskMap;

   @Inject
   CrewMate(@QualifiedMap Map<String, Tasks> taskMap) {
     this.taskMap = taskMap;
   }

 }

Assisted Injection

Assisted injection is a dependency injection pattern used to construct an object where some parameters may be provided by the DI framework and others must be passed in at creation time (“assisted” if you will) by the user.

Avaje Inject will generate a factory implementation responsible for combining all of the parameters and creating the object.

@AssistFactory

To use assisted injection, we annotate our desired class with @AssistFactory to signal the generator to create a factory class. @AssistFactory requires an interface/abstract class type that has the assisted types as parameters.

To mark fields/method parameters as assisted, we annotate them with @Assisted, as shown below:

@AssistFactory(CarFactory.class)
public class Car {

  @Assisted Make make;
  @Inject Wheel wheel;

  public Car(@Assisted Paint paint, @Nullable Engine engine) {
  //  ...
  }

  //will be triggered after contruction
  @Inject
  void injectMethod(@Assisted int size, Steel steel) {
  //  ...
  }

  //Factory Type the generated code will implement
  public interface CarFactory {
    Car fabricate(Paint paint, int size, Make make);
  }
}

We can now wire a factory instance and use in our application

@Singleton
public class Dealer {

  CarFactory factory;

  @Inject
  public Dealer(CarFactory factory) {
    this.factory = factory;
  }


  Car orderCar(Order order) {
    //  ...
    return factory.fabricate(paint, size, make)
  }
}

Generated Code

Avaje Inject will read an assisted bean to generate a factory, here is the generated factory for the above Car class:

@Component
final class Car$AssistFactory implements CarFactory {

  @Inject
  Wheel wheel$field;

  private Steel steel$method;
  private final Engine engine;

  Car$AssistFactory(@Nullable Engine engine) {
    this.engine = engine;
  }

  /**
   * Fabricates a new Car.
   */
  @Override
  public Car construct(Paint paint, int size, Make make) {
    var bean = new Car(paint, engine);
    bean.wheel = wheel$field;
    bean.make = make;
    bean.injectMethod(size, steel$method);
    return bean;
  }

  @Inject
  void injectMethod(Steel steel) {
    this.m$method = steel;
  }
}

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 ...
    ...
  }
  ...

@PostConstruct with BeanScope

Since post construct methods execute after all the beans have been wired, we can also add the completed BeanScope as a parameter.

@Singleton
public class CoffeeMaker {
  Beans beans;

  @PostConstruct
  void onStartup(BeanScope scope) {
   beans = scope.get(Beans.class);
  }
  ...

@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
    ...
  }

  @PreDestroy(priority=20) //default value is 1000
  void onShutdownPriority() {
    // close resources in a specific order
    ...
  }
  ...

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()

Conditional Beans

@RequiresBean

Put @RequiresBean on a @Factory class, @Factory method, or @Singleton class so that a bean will only be registered when the conditions are met.

@Singleton
@RequiresBean(Kindling.class)
@RequiresBean(Cinders.class)
public class Fire {
   ...
}

@RequiresProperty

Put @RequiresProperty on a @Factory class, @Factory method, or @Singleton class so that a bean will only be registered when the conditions are met.

@Singleton
@RequiresProperty("unkindled")
@RequiresProperty(value = "fire", notEqualTo = "burning")
public class Dark {
   ...
}

@Profile

When the property avaje.profiles is set, we can use @Profile on a @Factory class, @Factory method, or @Singleton class so that a bean will only be registered when the given wiring profiles are present/absent.

@Singleton
@Profile("ds2")
public class BearerOfTheCurse {
   ...
}

PropertyRequiresPlugin

To test property/profile conditions, an instance of io.avaje.inject.spi.PropertyRequiresPlugin is loaded via java.util.ServiceLoader. If there are no PropertyRequiresPlugin found, a default implementation will be provided that uses System.getProperty(String) and System.getenv(String).

Avaje Config provides a PropertyRequiresPlugin, so when it's detected in the classpath, it will be used to test the property conditions.

You can provide your own implementation of PropertyRequiresPlugin via service loader if you want to use your own custom testing of property condition.

Condition Meta-Annotations

If multiple beans require the same combination of requirements, you can define a meta-annotation with the requirements:
@RequiresBean(Flame.class)
@RequiresBean(value = Kindling.class, missing = Dark.class)
@RequiresProperty(value = "flame.state", notEqualTo = "fading")
public @interface FirstFlame {}
These annotation can be placed on beans to easily share conditions.
@Singleton
@FirstFlame
@RequiresProperty(value = "flame.state", notEqualTo = "fading")
public class Light {
   ...
}
Additionally, meta annotation can be placed on other meta annotation to easily compose multiple related conditions.
@FirstFlame
@RequiresProperty(value = "abyss", equalTo="sealed")
public @interface AgeOfFire {}
@Singleton
@AgeOfFire
// AgeOfFire effectively adds the following conditions
// @RequiresProperty(value = "abyss", equalTo="sealed")
// @RequiresBean(Flame.class)
// @RequiresBean(value = Kindling.class, missing = Dark.class)
// @RequiresProperty(value = "flame.state", notEqualTo = "fading")
public class Sun {
   ...
}

Configuration Requirements

The conditional annotations are pretty flexible and can be used for a variety of use cases. The following table summarizes some common uses:

Requirement Example
One or more beans should be present @RequiresBean({DataSource.class, DBClient.class})
One or more beans should not be present @RequiresBean(missing = {DataSource.class, DBClient.class})
Beans with the given names/qualifiers should be present @RequiresBeans(qualifiers = {"blue", "green"})
@PropertyRequires
A given property exists @RequiresProperty("spinning")
Given properties don't exist @RequiresProperty(missing = {"spiral","nemesis"})
Given property equals a value @RequiresProperty(value = "drill", equalTo = "spin-on")
Given property does not equal a value @RequiresProperty(value = "spirit", notEqualTo = "broken")
@Profile
Any of the given profiles are set @Profile({"sword","bow"})
Given profiles are not set @Profile(none = {"malice","gloom"})
All the given profiles must be set @Profile(all = {"light","dragon"})
Given property does not equal a value @Profile(value = "spirit", notEqualTo = "broken")

Aspect Oriented Programming

This library has several contructs that support Aspect Oriented Programmming

@Aspect

Create an annotation class and annotate it with @Aspect to define an aspect annotation. To control the execution order of multiple aspects, we can use the ordering property of the @Aspect. To import an existing annotation, use @Aspect.Import.

@Aspect(ordering=1) // Determines priority among other aspects
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAround {

  String name() default "";
}

For this aspect to work, a corresponding AspectProvider must be wired into the scope. The AspectProvider should be a @Singleton or @Component that provides a MethodInterceptor. (Which will intercept the method call).

@Singleton
public class MyAroundAspect implements AspectProvider<MyAround> {

  @Override
  public MethodInterceptor interceptor(Method method, MyAround around) {
    return new ExampleInterceptor();
  }

  static class ExampleInterceptor implements MethodInterceptor {
    // MethodInterceptor interception method
    @Override
    public void invoke(Invocation invoke) throws Throwable {
      System.out.println("before args: " + Arrays.toString(invoke.arguments()) + " method: " + invoke.method());
      try {
        invoke.invoke();
      } finally {
        System.out.println("after");
      }
    }
  }
}

With the provider set, we can use our newly created aspect annotation on a class/method to intercept calls.

@Singleton
public class ExampleService {

  @MyAround
  public String example(String param0, int param1) {
    return "other " + param0 + " " + param1;
  }
}

Avaje will generate a proxy class that will run the aspects for every annotated method.

@Proxy
@Generated("io.avaje.inject.generator")
public class ExampleService$Proxy extends ExampleService {

  private final MyAroundAspect myAroundAspect;

  private Method example0;
  private MethodInterceptor example0MyAround;

  public ExampleService$Proxy(MyAroundAspect myAroundAspect) {
    super();
    this.myAroundAspect = myAroundAspect;
    try {
      example0 = ExampleService.class.getDeclaredMethod("example", String.class, int.class);
      example0MyAround = myAroundAspect.interceptor(example0, example0.getAnnotation(MyAround.class));
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }
  }

  @Override
  public String example(String param0, int param1) {
    var call = new Invocation.Call<>(() -> super.example(param0, param1))
      .with(this, example0, param0, param1);
    try {
      example0MyAround.invoke(call);
      return call.finalResult();
    } catch (InvocationException e) {
      throw e;
    } catch (Throwable e) {
      throw new InvocationException(e);
    }
  }
}

@AOPFallback

We can use @AOPFallback to register a fallback method for an aspect method invocation. Recovery methods must return the same type as the target method and may have 4 options for arguments:

@Singleton
class ExampleService {

  @MyAround
  public String example(String param0, int param1) {
    throw new IllegalStateException();
  }

  @AOPFallback("example")
  public String fallback(String param0, int param1, Throwable e) {
    return "fallback-" + param0 + ":" + param1 + ":" + e.getMessage();
  }
}

Inside our method interceptor, we can use Invocation#invokeRecoveryMethod to recover from an exception.

@Singleton
public class MyAroundAspect implements AspectProvider<MyAround> {

  //rest of aspect provider...

  static class ExampleInterceptor implements MethodInterceptor {
    // MethodInterceptor interception method
    @Override
    public void invoke(Invocation invoke) throws Throwable {
      System.out.println("before args: " + Arrays.toString(invoke.arguments()) + " method: " + invoke.method());
      try {
        invoke.invoke();
      } catch(Exception ex) {
        //recover
        invoke.invokeRecoveryMethod(ex);
      } finally {
        System.out.println("after");
      }
    }
  }
}

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 = ...).

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.

Modules

To wire all the beans into a scope, avaje-inject generates module classes that run all the constructors/factory methods and adds all beans to the scope.

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.

autoRequires

avaje-inject can also automatically read the classpath/maven dependencies at compile-time to find all the modules and automatically determine the requires dependencies. This works fine in most cases, but when you are using the annotation processor with a java 9+ modular project or defined as an annotationProcessorPath in the maven-compiler-plugin, you will need to add the avaje-inject-maven-plugin. (For Gradle, use the avaje-inject-gradle-plugin)

<plugin>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-inject-maven-plugin</artifactId>
  <version>1.2</version>
  <executions>
    <execution>
      <phase>process-sources</phase>
      <goals>
        <goal>provides</goal>
      </goals>
    </execution>
  </executions>
</plugin>

What this does is generate 2 files in target before the code is compiled: target/avaje-module-provides.txt and target/avaje-plugin-provides.txt These are the components and plugins provides by all the other modules that exist in the classpath/maven dependencies. The annotation processor then reads the txt files at compile time and will not error if these components are required dependencies (as they are known to be provided by other modules or plugins).

Shading Note

As avaje uses the ServiceLoader to load Module classes, be sure to have the following configuration set when using the maven shade plugin on multi-module projects.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <configuration>
    <transformers>
      <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
    </transformers>
  </configuration>
</plugin>

This ensures that the META-INF/services files in the shaded dependencies are merged into the UberJar. With all service entries merged, avaje can discover and load all available modules.

@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".

Plugins

If you want to execute code when creating a bean scope, you can implement the Plugin SPI. Typically, a plugin might provide a default dependency via BeanScopeBuilder.provideDefault().

Plugins implement the io.avaje.inject.spi.Plugin interface and are found and registered via ServiceLoader. This means they have a file at src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin which contains the fully qualified class name of the implementation.

Below is an example plugin that provides a default ExampleBean instance.

public final class DefaultBeanProvider implements io.avaje.inject.spi.Plugin {

  //this is called at compile time to tell avaje what bean classes (if any) this plugin provides
  @Override
  public Class<?>[] provides() {
    return new Class<?>[]{ExampleBean.class};
  }

  // this is called at runtime before any beans are wired
  @Override
  public void apply(BeanScopeBuilder builder) {
    //you can access the scope's wiring properties to help configure your plugin
    var props = builder.propertyPlugin();

    //provide a default bean
    builder.provideDefault(ExampleBean.class, ExampleBean::new);
  }
}

Testing

Unit Testing

When we are unit testing we are focused on the thing we want to test (object under test) and it's dependencies.

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>${avaje.inject.version}</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;
  // Get this OUT of the DI BeanScope
  @Inject CoffeeMaker coffeeMaker;
  //When a field annotated @Inject has an initialized value, it's wired INTO the DI BeanScope
  @Inject BeanService myTestDouble = new BeanService();

  @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, we 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.

@Setup

We can use a method annotated with @Setup as an alternative to mock annotations to provide mocks to the test bean context.

@InjectTest
class Inject_Test {
  // calls repo and adds hello string
  @Inject
  Service service;

  Repo repoDouble;

  //use setup to add/replace beans in the context before tests
  @Setup
  void setup(BeanScopeBuilder builder) {
    repoDouble = mock(Repo.class);
    when(repoDouble.get()).thenReturn("MockedViaSetupMethod");
    builder.bean(Repo.class, repoDouble);
  }

  @Test
  void test() {
    assertEquals("MockedViaSetupMethod+hello", service.process());
  }
}

@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.

@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();
  }
}

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 was one of the first dependency injection libraries that used Java annotation processing to generate code for DI. This moved work that was previously done at runtime to build time, which 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. Spring heavily relies on classpath scanning, reflection and defining dynamic proxies. This makes it relatively slow and hungry for both CPU and memory resources. In cloud deployment where we pay for CPU and memory, some developers started looking for another approach to DI which moved 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 based on CDI.

Quick comparison to other DI libraries

Why not use Dagger2?

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

Why not use Quarkus?

Quarkus comes with a DI implementation based on CDI. If CDI is your thing you'd look at Quarkus. Even so, avaje-Inject is a much smaller library that's focused entirely on DI.

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 for readability and excluded @Value with a preference instead to use a simple avaje-config style approach to external configuration. In addition, avaje determines wiring order at compile-time, hence it doesn't need to take time to figure out all the dependency relationships at runtime.

Ultimately, avaje-inject is a relatively tiny library in comparison to Micronaut or Quarkus 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/Nima.

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
@Conditional @RequiresBean and @RequiresProperty No
@Primary @Primary No
@Profile @Profile No
@Secondary @Secondary No

 

 

Spring DI translation NOT part of avaje-inject

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

Why we don't have @Value/Refreshable Scopes

Both Spring and Micronaut have a @Value and by implication, they have chosen to combine "external configuration" in with "dependency injection". With avaje-inject a design decision was made to keep these two ideas separate. In theory we could have implemented this via source code generation with avaje-inject as:

public class EngineImpl$Proxy extends EngineImpl {

  public EngineImpl$Proxy() { // match super constructor,
    super();
    this.cylinders = Config.getInt("my.engine.cylinders", 6);
  }
}

Our reasons for not implementing are as follows.

Timing of setting the configuration

We can see that cylinders is only set after the super(). Any code that tries to use cylinders before that would get a 0 (or null with Integer etc). This is relatively obvious to experienced devs but it is a source of bugs for less experienced devs. That is, @Value fields have delayed initialization and this can trip people up / be a source of bugs.

If we don't use @Value and use Config.getInt() directly on the field, the behavior is completly unambiguous. The values are initialized like any normal field.

Dynamic Configuration

With avaje-inject we create an effectively immutable BeanScope because we expect "external dynamic configuration" to be done independently from Dependency Injection (for example, by using avaje-config). If we go from needing the configuration read and set once at startup to being read each time and potentially changing (aka dynamic configuration). Then we'd need to change away from using @Value or add a complex "Refreshable Scope" concept to this llibrary.

When using Config.getInt() directly, there is more freedom. We can use it anywhere - field, final field, static final field, in a method (dynamic configuration). There isn't a big shift between static configuration and dynamic configuration.

Freedom

We have an excellent configuration library in avaje-config. It's simple, extendable, and mature as it was originally part of Ebean ORM and was extracted into it's own project.

Even so, we want to give our users the freedom to choose whatever they like with external configuration libraries. If we supported @Value in avaje-inject then we would have to pick a "configuration implementation" and force our avaje-config dependency where it's potentially unwanted.