概述

本文将介绍由ebean创建者开发的JVM基础框架——Avaje Inject,它是一种高级编译时依赖注入(DI)解决方案。Avaje采用生成可读源代码的方式支持各种DI操作,它能读取带有JSR-330注解的bean,并自动生成类,用于在适当的时候收集应用中的bean实例。

依赖注入

作为对控制反转原则的一个具体应用,依赖注入(Dependency Injection,DI)是程序控制其自身流程的一种方式。

不同的框架以不同的方式实现依赖注入,其中一个显著的区别在于注入是在运行时还是编译时进行。Avaje选择完全依赖注解处理进行编译时源代码生成。

Maven/Gradle 配置

要在项目中使用Avaje Inject,我们需要将avaje-inject依赖项添加到pom.xml中:

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

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

此外,我们还需要包含inject代码生成器,以便读取我们的注解类并生成用于注入的代码。由于生成器仅在编译时需要,所以它是可选的,在Maven中添加为optional依赖:

如果使用Gradle,我们将这样添加依赖:

implementation('io.avaje:avaje-inject:9.5')
annotationProcessor('io.avaje:avaje-inject-generator:9.5')

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

现在项目中有了Avaje Inject,让我们创建一个示例来了解它是如何工作的。

实现

以构建一个依赖于武器的骑士为例。Avaje Inject的生成器会在编译时读取所有不同注解,并在target/generated-sources/annotations目录下生成DI代码。

4.1. *@Singleton* 和 *@Inject*

Avaje与Spring的\@Component类似,使用标准JSR-330注解\@Singleton标记一个类为bean。为了注入依赖,我们在字段或构造函数上添加\@Inject注解。生成的DI代码位于同一个包中,因此为了使用字段/方法注入,这些元素必须是公有或包私有。

看看我们的Knight类,它有两个武器依赖:

@Singleton
public class Knight {

  private Sword sword;

  private Shield shield;

  @Inject
  public Knight(Sword sword, Shield shield) {
    this.sword = sword;
    this.shield = shield;
  }
  //standard getters and setters
}

代码生成器会读取注解并生成一个类,用于收集依赖并注册bean:

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

  public static void build(Builder builder) {
    if (builder.isAddBeanFor(Knight.class)) {
      var bean = new Knight(builder.get(Sword.class,"!sword"), builder.get(Shield.class,"!shield"));
      builder.register(bean);
    }
  }
}

这个类检查是否存在Knight类,然后从当前作用域中获取依赖。

4.2. *@Factory* 和 *@Bean*

类似于Spring的\@Configuration类,我们可以将类注解为\@Factory,并在其方法上注解\@Bean,使其成为创建bean的工厂。

ArmsFactory类中,我们使用这种方式为应用作用域提供骑士的武器:

@Factory
public class ArmsFactory {

  @Bean
  public Sword provideSword() {
    return new Sword();
  }

  @Bean
  public Brand provideShield() {
    return new Shield(25);
  }
}

代码生成器会读取注解并生成一个类,用于调用构造函数和工厂方法:

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

  public static void build(Builder builder) {
    if (builder.isAddBeanFor(ArmsFactory.class)) {
      var bean = new ArmsFactory();
      builder.register(bean);
    }
  }

  public static void build_provideEngine(Builder builder) {
    if (builder.isAddBeanFor(Sword.class)) {
      var factory = builder.get(ArmsFactory.class);
      var bean = factory.provideEngine();
      builder.register(bean);
    }
  }

  public static void build_provideBrand(Builder builder) {
    if (builder.isAddBeanFor(Shield.class)) {
      var factory = builder.get(ArmsFactory.class);
      var bean = factory.provideBrand();
      builder.register(bean);
    }
  }
}

4.3. *@PostConstruct* 和 *@PreDestroy*

Avaje可以使用生命周期方法附加自定义操作到bean的创建和销毁过程。@PostConstruct方法在BeanScope完成所有bean的连接后执行,而@PreDestroyBeanScope关闭时运行。

由于@PostConstruct方法在所有bean连接后执行,我们可以在BeanScope参数中配置更多功能,使用已完成的BeanScope。下面的Ninja类使用@PostConstruct设置其成员,从应用作用域中获取bean:

@Singleton
public class Ninja {

  private Sword sword;

  @PostConstruct
  void equip(BeanScope scope) {
    sword = scope.get(Sword.class);
  }

  @PreDestroy
  void dequip() {
    sword = null;
  }

//getters/setters
}

代码生成器会读取注解并生成一个类,用于调用构造函数并注册生命周期方法:

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

  public static void build(Builder builder) {
    if (builder.isAddBeanFor(Ninja.class)) {
      var bean = new Ninja();
      var $bean = builder.register(bean);
      builder.addPostConstruct($bean::equip);
      builder.addPreDestroy($bean::dequip);
    }
  }
}

生成的 Module*

在编译时,avaje-inject-generator读取所有bean定义并确定所有bean的连接。然后,它会生成一个表示应用及其依赖的Module类。

对于上述所有类,以下IntroModule被生成,用于执行所有注入,添加到应用作用域中。我们可以看到应用中所有bean的定义和连接顺序:

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

  private Builder builder;

  @Override
  public Class<?>[] classes() {
    return new Class<?>[]{
      com.baeldung.avaje.intro.ArmsFactory.class,
      com.baeldung.avaje.intro.Knight.class,
      com.baeldung.avaje.intro.Ninja.class,
      com.baeldung.avaje.intro.Shield.class,
      com.baeldung.avaje.intro.Sword.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;
    build_intro_ArmsFactory();
    build_intro_Ninja();
    build_intro_Sword();
    build_intro_Shield();
    build_intro_Knight();
  }

  @DependencyMeta(type = "com.baeldung.avaje.intro.ArmsFactory")
  private void build_intro_ArmsFactory() {
    ArmsFactory$DI.build(builder);
  }

  @DependencyMeta(type = "com.baeldung.avaje.intro.Ninja")
  private void build_intro_Ninja() {
    Ninja$DI.build(builder);
  }

  @DependencyMeta(
      type = "com.baeldung.avaje.intro.Sword",
      method = "com.baeldung.avaje.intro.ArmsFactory$DI.build_provideSword",
      dependsOn = {"com.baeldung.avaje.intro.ArmsFactory"})
  private void build_intro_Sword() {
    ArmsFactory$DI.build_provideSword(builder);
  }

  @DependencyMeta(
      type = "com.baeldung.avaje.intro.Shield",
      method = "com.baeldung.avaje.intro.ArmsFactory$DI.build_provideShield",
      dependsOn = {"com.baeldung.avaje.intro.ArmsFactory"})
  private void build_intro_Shield() {
    ArmsFactory$DI.build_provideShield(builder);
  }

  @DependencyMeta(
      type = "com.baeldung.avaje.intro.Knight",
      dependsOn = {
        "com.baeldung.avaje.intro.Sword",
        "com.baeldung.avaje.intro.Shield"
      })
  private void build_intro_Knight() {
    Knight$DI.build(builder);
  }
}

通过 BeanScope 获取bean

为了管理依赖,Avaje的BeanScope加载并执行来自应用及其依赖的所有生成的Module类,并存储创建的bean供稍后检索。

让我们构建BeanScope,并获取装备有SwordShieldKnight类:

final var scope = BeanScope.builder().build();
final var knight = scope.get(Knight.class);

assertNotNull(knight);
assertNotNull(knight.sword());
assertNotNull(knight.shield());
assertEquals(25, knight.shield().defense());

测试中的组件使用 *@InjectTest*

当我们需要为测试启动bean作用域时,\@InjectTest注解非常有用。该注解通过创建一个测试BeanScope来工作,该作用域将在测试中使用。

我们可以在测试的BeanScope中使用Mockito的\@Mock注解添加mock对象。当我们在字段上使用\@Mock注解时,mock将注入到字段中,并在测试作用域中注册。如果测试作用域中存在同类型的现有bean,mock将替换它。如果没有,将添加一个新的bean。这对于需要模拟特定bean(如外部服务)的组件测试非常有用。

这里,我们使用注入的Shield mock来模拟防御方法。然后,我们使用\@Inject从测试作用域中获取骑士bean,以验证它包含了mocked的盾牌:

@InjectTest
class ExampleInjectTest {

  @Mock Shield shield;

  @Inject Knight knight;

  @Test
  void givenMockedShield_whenGetShield_thenShieldShouldHaveMockedValue() {

    Mockito.when(shield.defense()).thenReturn(0);
    assertNotNull(knight);
    assertNotNull(knight.sword());
    assertEquals(knight.shield(), shield);
    assertEquals(0, knight.shield().defense());
  }
}

结论

在这篇文章中,我们介绍了如何设置和使用Avaje Inject的基本示例。我们看到了它是如何通过代码生成执行各种DI操作的,以及如何创建测试和使用mock。

如往常一样,文章中的所有代码都可在GitHub上找到。