概述
本文将介绍由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的连接后执行,而@PreDestroy
在BeanScope
关闭时运行。
由于@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
,并获取装备有Sword
和Shield
的Knight
类:
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上找到。