跳到主要内容

3. 控制反转(IoC)

与其他依赖于运行时反射和代理的框架不同,Micronaut 使用编译时数据来实现依赖注入。

这是 Google Dagger 等工具采用的类似方法,该工具的设计主要考虑到了 Android。另一方面,Micronaut 是为构建服务器端微服务而设计的,它提供了许多与其他框架相同的工具和实用程序,但没有使用反射或缓存过多的反射元数据。

Micronaut IoC 容器的目标概括如下:

  • 将自省作为最后手段
  • 避免代理
  • 优化启动时间
  • 减少内存占用
  • 提供清晰易懂的错误处理

请注意,Micronaut 的 IoC 部分可以完全独立于 Micronaut 用于你希望构建的任何应用程序类型。

为此,请将构建配置为包含 micronaut-inject-java 依赖项作为注解处理器。

最简单的方法是使用 Micronaut 的 Gradle 或 Maven 插件。例如 Gradle:

配置 Gradle

plugins {
id 'io.micronaut.library' version '1.3.2' (1)
}

version "0.1"
group "com.example"

repositories {
mavenCentral()
}

micronaut {
version = "3.9.4" (2)
}
  1. 定义 Micronaut Library 插件
  2. 指定使用的 Micronaut 版本

IoC 的入口点是 ApplicationContext 接口,它包括一个 run 方法。以下示例演示如何使用它:

运行 ApplicationContext

try (ApplicationContext context = ApplicationContext.run()) { (1)
MyBean myBean = context.getBean(MyBean.class); (2)
// do something with your bean
}
  1. 运行 ApplicationContext
  2. 从 ApplicationContext 获取一个 bean
注意

该示例使用 Java try-with-resources 语法来确保 ApplicationContext 在应用程序退出时完全关闭

3.1 定义 Bean

bean 是一个对象,其生命周期由 Micronaut IoC 容器管理。该生命周期可能包括创建、执行和销毁。Micronaut 实现了 JSR-330(javax.inject—— Java 依赖注入 规范,因此要使用 Micronaut,只需使用 javax.inject 提供的注解即可。

以下是一个简单的示例:

public interface Engine { // (1)
int getCylinders();
String start();
}

@Singleton// (2)
public class V8Engine implements Engine {
private int cylinders = 8;

@Override
public String start() {
return "Starting V8";
}

@Override
public int getCylinders() {
return cylinders;
}

public void setCylinders(int cylinders) {
this.cylinders = cylinders;
}
}

@Singleton
public class Vehicle {
private final Engine engine;

public Vehicle(Engine engine) {// (3)
this.engine = engine;
}

public String start() {
return engine.start();
}
}
  1. 定义了通用 Engine 接口
  2. V8Engine 实现被定义并标记为 Singleton 作用域
  3. 通过构造函数注入来注入 Engine

要执行依赖注入,请使用 run() 方法运行 BeanContext,并使用 getBean(Class) 查找 bean,如下例所示:

final BeanContext context = BeanContext.run();
Vehicle vehicle = context.getBean(Vehicle.class);
System.out.println(vehicle.start());

Micronaut 自动发现 classpath 上的依赖注入元数据,并根据你定义的注入点将 bean 连接在一起。

Micronaut 支持以下类型的依赖注入:

  • 构造函数注入(必须是一个公共构造函数或带有 @Inject 注解的单个构造函数)
  • 字段注入
  • JavaBean 属性注入
  • 方法参数注入

3.2 它如何工作?

此时,你可能想知道 Micronaut 如何在不需要反射的情况下执行上述依赖注入。

关键是一组 AST 转换(对于 Groovy)和注解处理器(对于 Java),它们生成实现 BeanDefinition 接口的类。

Micronaut 使用 ASM 字节码库来生成类,因为 Micronaut 提前知道注入点,所以不需要像其他框架,如 Spring 那样在运行时扫描所有方法、字段、构造函数等。

此外,由于在构建 bean 时不使用反射,JVM 可以更好地内联和优化代码,从而提高运行时性能并减少内存消耗。这对于应用程序性能依赖于 bean 创建性能的非单例作用域尤为重要。

此外,使用 Micronaut,你的应用程序启动时间和内存消耗不会受到代码库大小的影响,与使用反射的框架一样。基于反射的 IoC 框架为代码中的每个字段、方法和构造函数加载和缓存反射数据。因此,随着代码大小的增加,内存需求也会随之增加,而使用 Micronaut 时情况并非如此。

3.3 BeanContext

BeanContext 是所有 bean 定义的容器对象(它还实现了 BeanDefinitionRegistry)。

这也是 Micronaut 的初始化点。然而,一般来说,你不直接与 BeanContext API 交互,只需使用 jakarta.inject 注解和 io.micronaut.context.annotation 包中的注解即可满足依赖注入需求。

3.4 可注入容器类型

除了能够注入 bean 之外,Micronaut 还支持注入以下类型:

表1. 可注入容器类型

类型描述示例
java.util.OptionalOptional 的 bean。如果 bean 不存在,注入 empty()Optional<Engine>
java.lang.CollectionCollectionCollection 子类型(如,ListSet 等)Collection<Engine>
java.util.stream.StreamStream 的 beanStream<Engine>
Array给定类型的 bean 的本地数组Engine[]
Provider如果循环依赖需要它,是一个 javax.inject.Provider,或者为每个 get 调用实例化一个原型。Provider<Engine>
Provider如果循环依赖需要它,是一个 jakarta.inject.Provider,或者为每个 get 调用实例化一个原型。Provider<Engine>
BeanProvider如果循环依赖需要它,是一个 io.micronaut.context.BeanProvider,或者为每个 get 调用实例化一个原型。BeanProvider<Engine>
注意

支持 3 种不同的提供器类型,但我们建议使用 BeanProvider

注意

当将 java.lang.Collectionjava.util.stream.streamArray 的 bean 注入到与注入类型匹配的 bean 中时,所属 bean 将不会是注入集合的成员。证明这一点的一种常见模式是聚合。例如:

@Singleton
class AggregateEngine implements Engine {
@Inject
List<Engine> engines;

@Override
public void start() {
engines.forEach(Engine::start);
}

...
}

在此示例中,注入的成员变量 engines 将不包含 AggregateEngine 的实例

提示

原型 bean 将在注入 bean 的每个位置创建一个实例。当将原型bean作为提供器注入时,每次调用 get() 都会创建一个新实例。

集合排序

在注入 bean 集合时,默认情况下不会对它们进行排序。实现 Ordered 接口以注入有序集合。如果请求的 bean 类型未实现 Ordered,Micronaut 将在 bean 上搜索 @Order 注解。

@Order 注解特别适用于对工厂创建的 bean 进行排序,其中 bean 类型是第三方库中的类。在此示例中,LowRateLimitHighRateLimit 都实现 RateLimit 接口。

带 @Order 的工厂

import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Order;

import jakarta.inject.Singleton;
import java.time.Duration;

@Factory
public class RateLimitsFactory {

@Singleton
@Order(20)
LowRateLimit rateLimit2() {
return new LowRateLimit(Duration.ofMinutes(50), 100);
}

@Singleton
@Order(10)
HighRateLimit rateLimit1() {
return new HighRateLimit(Duration.ofMinutes(50), 1000);
}
}

当从上下文请求 RateLimit bean 集合时,将根据注解中的值以升序返回它们。

按顺序注入 Bean

当注入 bean 的单个实例时,@Order 注解也可以用于定义哪个 bean 具有最高优先级,因而应该注入。

注意

选择单个实例时不考虑 Ordered 接口,因为这需要实例化 bean 来解析顺序

3.5 Bean 限定符

如果给定接口有多个可能的实现要注入,则需要使用限定符。

Micronaut 再次利用 JSR-330 以及 QualifierNamed 注解来支持此用例。

按名字限定

要按名称限定,请使用 Named 注解。例如,考虑以下类:

public interface Engine { // (1)
int getCylinders();
String start();
}

@Singleton
public class V6Engine implements Engine { // (2)
@Override
public String start() {
return "Starting V6";
}

@Override
public int getCylinders() {
return 6;
}
}

@Singleton
public class V8Engine implements Engine { // (3)
@Override
public String start() {
return "Starting V8";
}

@Override
public int getCylinders() {
return 8;
}

}

@Singleton
public class Vehicle {
private final Engine engine;

@Inject
public Vehicle(@Named("v8") Engine engine) {// (4)
this.engine = engine;
}

public String start() {
return engine.start();// (5)
}
}
  1. Engine 接口定义通用合同
  2. V6Engine 类是第一个实现
  3. V8Engine 类是第二个实现
  4. javax.inject.Named 注解表示需要 V8Engine 实现
  5. 调用 start 方法打印:"Starting V8"

Micronaut 能够在前面的示例中注入 V8Engine,因为:

@Named 限定符(v8)+ 注入的类型简单名称(Engine)==(不区分大小写)== Engine 类型的 bean 的简单名称(V8Engine

你还可以在 bean 的类级别声明 @Named,以显式定义 bean 的名称。

按注解限定

除了能够按名称限定外,还可以使用 Qualifier 注解构建自己的限定符。例如,考虑以下注解:

import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}

上面的注解本身使用 @Qualifier 进行注解,以将其指定为限定符。然后可以在代码中的任何注入点使用注解。例如:

@Inject Vehicle(@V8 Engine engine) {
this.engine = engine;
}

按注解成员限定

从 Micronaut 3.0 开始,注解限定符也可以使用注解成员来解决正确的 bean 注入。例如,考虑下面这个注解:

import io.micronaut.context.annotation.NonBinding;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier // (1)
@Retention(RUNTIME)
public @interface Cylinders {
int value();

@NonBinding // (2)
String description() default "";
}
  1. @Cylinders 注解使用 @Qualifier 进行元注解
  2. 注解有两个成员。@NonBinding 注解用于在依赖项解析期间排除描述成员。

然后,你可以在任何 bean 上使用 @Cylinders 注解,并且在依赖关系解析期间会考虑未使用 @NonBinding 注解的成员:

@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine") // (1)
public class V6Engine implements Engine { // (2)

@Override
public int getCylinders() {
return 6;
}

@Override
public String start() {
return "Starting V6";
}
}
  1. 此处,V6Engine 类型的 value 成员设置为 6
  2. 该类实现 Engine 接口
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
public class V8Engine implements Engine { // (2)
@Override
public int getCylinders() {
return 8;
}

@Override
public String start() {
return "Starting V8";
}
}
  1. 这里,V8Engine 类型的 value 成员设置为 8
  2. 该类实现 Engine 接口

然后可以在任何注入点上使用 @Cylinder 限定符来选择要注入的正确 bean。例如:

@Inject Vehicle(@Cylinders(8) Engine engine) {
this.engine = engine;
}

按泛型类型参数限定

从 Micronaut 3.0 开始,可以根据类或接口的泛型类型参数选择要注入的 bean。考虑以下示例:

public interface CylinderProvider {
int getCylinders();
}

CylinderProvider 接口提供 cylinder 数。

public interface Engine<T extends CylinderProvider> { // (1)
default int getCylinders() {
return getCylinderProvider().getCylinders();
}

default String start() {
return "Starting " + getCylinderProvider().getClass().getSimpleName();
}

T getCylinderProvider();
}
  1. 引擎类定义了一个泛型类型参数 <T>,该参数必须是 CylinderProvider 的实例

你可以使用不同的泛型类型参数定义 Engine 接口的实现。例如,对于 V6 engine:

public class V6 implements CylinderProvider {
@Override
public int getCylinders() {
return 6;
}
}

上面定义了一个实现 CylinderProvider 接口的 V8 类:

@Inject
public Vehicle(Engine<V8> engine) {
this.engine = engine;
}

首选及备选 Bean

Primary 是一个限定符,表示在多个接口实现的情况下,bean 要选择的首选 bean。

考虑以下示例:

public interface ColorPicker {
String color();
}

ColorPicker 由以下类实现:

首选 Bean

import io.micronaut.context.annotation.Primary;
import jakarta.inject.Singleton;

@Primary
@Singleton
class Green implements ColorPicker {

@Override
public String color() {
return "green";
}
}

Green bean 类实现 ColorPicker,并用 @Primary 注解。

同类型的另一个 Bean

import jakarta.inject.Singleton;

@Singleton
public class Blue implements ColorPicker {

@Override
public String color() {
return "blue";
}
}

Blue bean 类还实现了 ColorPicker,因此在注入 ColorPicker 接口时有两个可能的候选对象。由于 Green 是首选的,因此它将一直受到青睐。

@Controller("/testPrimary")
public class TestController {

protected final ColorPicker colorPicker;

public TestController(ColorPicker colorPicker) { // (1)
this.colorPicker = colorPicker;
}

@Get
public String index() {
return colorPicker.color();
}
}
  1. 虽然有两个 ColorPicker bean,但由于 @Primary 注解,Green 被注入

如果存在多个可能的候选项,并且未定义 @Primary,则引发 NonUniqueBeaException

除了 @Primary,还有一个 Secondary 注解,它会产生相反的效果,并允许取消 bean 的优先级。

注入任意 Bean

如果你不知道注入哪个 bean,那么可以使用 @Any 限定符来注入第一个可用的 bean,例如:

注入任意 Bean

@Inject @Any
Engine engine;

@Any 限定符通常与 BeanProvider 接口一起使用,以允许更动态的用例。例如,如果 bean 存在,以下 Vehicle 实现将启动 Engine

带 Any 使用 BeanProvider

import io.micronaut.context.BeanProvider;
import io.micronaut.context.annotation.Any;
import jakarta.inject.Singleton;

@Singleton
public class Vehicle {
final BeanProvider<Engine> engineProvider;

public Vehicle(@Any BeanProvider<Engine> engineProvider) { // (1)
this.engineProvider = engineProvider;
}
void start() {
engineProvider.ifPresent(Engine::start); // (2)
}
}
  1. 使用 @Any 注入 BeanProvider
  2. 如果使用 ifPresent 方法存在基础 bean,则调用 start 方法

如果有多个 bean,你也可以调整行为。以下示例启动 Vehicle 中安装的所有发动机(如果有):

带 Any 使用 BeanProvider

void startAll() {
if (engineProvider.isPresent()) { // (1)
engineProvider.stream().forEach(Engine::start); // (2)
}
}
  1. 检查是否有 bean
  2. 如果是这样,则通过 stream().forEach(..) 方法迭代每个引擎,启动引擎

3.6 限制可注入类型

默认情况下,当你使用作用域(如 @Singleton)注解 bean 时,bean 类及其实现的所有接口和扩展的超级类都可以通过 @Inject 注入。

考虑上一节中关于定义 bean 的以下示例:

@Singleton
public class V8Engine implements Engine { // (3)
@Override
public String start() {
return "Starting V8";
}

@Override
public int getCylinders() {
return 8;
}

}

在上述情况下,应用程序中的其他类可以选择注入接口 Engine 或具体实现 V8Engine

如果这是不希望的,可以使用 @Bean 注解的 typed 成员来限制公开的类型。例如:

@Singleton
@Bean(typed = Engine.class) // (1)
public class V8Engine implements Engine { // (2)
@Override
public String start() {
return "Starting V8";
}

@Override
public int getCylinders() {
return 8;
}
}
  1. @Bean(typed=..) 用于仅允许注入接口 Engine,而不允许注入具体类型
  2. 该类必须实现由 typed 定义的类或接口,否则将发生编译错误

以下测试演示了使用编程查找和 BeanContext API 进行 typed 的行为:

@MicronautTest
public class EngineSpec {
@Inject
BeanContext beanContext;

@Test
public void testEngine() {
assertThrows(NoSuchBeanException.class, () ->
beanContext.getBean(V8Engine.class) // (1)
);
final Engine engine = beanContext.getBean(Engine.class); // (2)
assertTrue(engine instanceof V8Engine);
}
}
  1. 尝试查找 V8Engine 引发 NoSuchBeaException
  2. 查找 Engine 接口时成功

3.7 作用域

Micronaut 具有基于 JSR-330 的可扩展 bean 作用域机制。支持以下默认作用域:

3.7.1 内置作用域

表 1.Micronaut 内置作用域

类型描述
@SingletonSingleton 作用域表示只存在bean的一个实例
@ContextContext 作用域表示 bean 将与 ApplicationContext 同时创建(急切初始化)
@PrototypePrototype 作用域表示每次注入 bean 时都会创建一个新的 bean 实例
@InfrastructureInfrastructure 作用域表示不能使用 @Replaces 重写或替换的 bean,因为它对系统的运行至关重要。
@ThreadLocal@ThreadLocal 作用域是一个自定义作用域,通过 ThreadLocal 为每个线程关联一个 bean
@Refreshable@Refreshable 作用域是一个自定义作用域,允许通过 /refresh 端点刷新bean的状态。
@RequestScope@RequestScope 作用域是一个自定义作用域,它指示创建了 bean 的新实例并与每个 HTTP 请求相关联
注意

@Prototype 注解是 @Bean 的同义词,因为默认作用域是 Prototype。

通过定义实现 CustomScope 接口的@Singleton bean,可以添加其他作用域。

注意,在启动 ApplicationContext 时,默认情况下,@Singleton 作用域 bean 是按需创建的。这是为了优化启动时间而设计的。

如果这给你的用例带来了问题,你可以选择使用 @Context 注解,该注解将对象的生命周期绑定到 ApplicationContext 的生命周期。换句话说,当 ApplicationContext 启动时,将创建 bean。

或者,用 @Parallel 注解任何 @Singleton 作用域的 bean,它允许并行初始化 bean 而不影响整个启动时间。

注意

如果 bean 未能并行初始化,应用程序将自动关闭。

3.7.1.1 Singleton 的急切初始化

在某些情况下,例如在 AWS Lambda 上,分配给 Lambda 构造的 CPU 资源多于执行的 CPU 资源,可能需要对 @Singleton bean 进行急切初始化。

你可以使用 ApplicationContextBuilder 接口指定是否急切地初始化 @Singleton 作用域的 bean:

启用单例的急切初始化

public class Application {

public static void main(String[] args) {
Micronaut.build(args)
.eagerInitSingletons(true) (1)
.mainClass(Application.class)
.start();
}
}
  1. 将急切初始化设置为 true 将初始化所有单例

无服务器函数等环境中使用 Micronaut 时,你将没有 Application 类,而是扩展了 Micronaut 提供的类。在这些情况下,Micronaut 提供了可以重写以增强 ApplicationContextBuilder 的方法

重载 newApplicationContextBuilder()

public class MyFunctionHandler extends MicronautRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
...
@Nonnull
@Override
protected ApplicationContextBuilder newApplicationContextBuilder() {
ApplicationContextBuilder builder = super.newApplicationContextBuilder();
builder.eagerInitSingletons(true);
return builder;
}
...
}

@ConfigurationReader bean,如 @EachProperty@ConfigurationProperties是单例 bean。要急切地初始化配置,但保持其他 @Singleton 作用域内的 bean 懒创建,请使用 eagerInitConfiguration

启用急切配置初始化

public class Application {

public static void main(String[] args) {
Micronaut.build(args)
.eagerInitConfiguration(true) (1)
.mainClass(Application.class)
.start();
}
}
  1. 将急切初始化设置为 true 将初始化所有配置读取器 bean

3.7.2 Refreshable 作用域

Refreshable 作用域是一个自定义作用域,允许通过以下方式刷新 bean 的状态:

以下示例说明了 @Refreshable 作用域的行为。

@Refreshable // (1)
public static class WeatherService {
private String forecast;

@PostConstruct
public void init() {
forecast = "Scattered Clouds " + new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(new Date());// (2)
}

public String latestForecast() {
return forecast;
}
}
  1. WeatherService 使用 @Refreshable 作用域进行注解,它存储实例,直到触发刷新事件
  2. 在创建 bean 时,forecast 属性的值设置为固定值,在刷新 bean 之前不会更改

如果你两次调用 latestForecast(),你将看到相同的响应,如 "Scattered Clouds 01/Feb/18 10:29.199"

当调用 /refresh 端点或发布 RefreshEvent 时,该实例将无效,并在下次请求对象时创建新实例。例如:

applicationContext.publishEvent(new RefreshEvent());

3.7.3 元注解作用域

可以在元注解上定义作用域,然后可以将其应用于类。考虑以下元注解示例:

Driver java 注解

import io.micronaut.context.annotation.Requires;

import jakarta.inject.Singleton;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Requires(classes = Car.class) // (1)
@Singleton // (2)
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
  1. 作用域使用 Requires 声明 Car 类上的需求
  2. 注解声明为 @Singleton

在上面的示例中,@Singleton 注解应用于 @Driver 注解,这会导致用 @Driver 进行注解的每个类都被视为单例。

注意,在这种情况下,应用注解时不可能更改作用域。例如,以下内容不会覆盖 @Driver 声明的作用域,并且无效:

声明另一个作用域

@Driver
@Prototype
class Foo {}

要使作用域可重写,请在 @Driver 上使用 DefaultScope 注解,如果没有其他作用域,则允许指定默认作用域:

使用 @DefaultScope

@Requires(classes = Car.class)
@DefaultScope(Singleton.class) (1)
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
  1. DefaultScope 声明了未指定时要使用的作用域

3.8 Bean 工厂

在许多情况下,你可能希望将不属于代码库的类(如第三方库提供的类)作为bean提供。在这种情况下,不能对编译的类进行注解。相反,实现一个 @Factory

工厂是一个用 Factory 注解的注解类,它提供了一个或多个注解的方法(用 bean 作用域注解)。你使用的注解取决于你希望 bean 位于哪个作用域中。更多信息,参阅 bean 作用域一节。

注意

工厂具有默认作用域 singleton ,并将随上下文一起销毁。如果你想在工厂生成 bean 后处理它,请使用 @Prototype 作用域。

用 bean 作用域注解来注解的方法的返回类型是 bean 类型。这最好用一个例子来说明:

@Singleton
class CrankShaft {
}

class V8Engine implements Engine {
private final int cylinders = 8;
private final CrankShaft crankShaft;

public V8Engine(CrankShaft crankShaft) {
this.crankShaft = crankShaft;
}

@Override
public String start() {
return "Starting V8";
}
}

@Factory
class EngineFactory {

@Singleton
Engine v8Engine(CrankShaft crankShaft) {
return new V8Engine(crankShaft);
}
}

在本例中,V8EngineEngineFactory 类的 V8Engine 方法创建。注意,你可以将参数注入到方法中,它们将被解析为 bean。生成的 V8Engine bean 将是一个单例。

一个工厂可以有多个用 bean 作用域注解的方法,每个方法都返回一个不同的 bean 类型。

注意

如果采用这种方法,则不应在类内部调用其他 bean 方法。相反,通过参数注入类型。

提示

要允许生成的 bean 参与应用程序上下文关闭过程,请使用 @Bean 注解该方法,并将 preDestroy 参数设置为要调用以关闭 bean 的方法的名称。

来自字段的 Bean

使用 Micronaut 3.0 或更高版本,也可以通过在字段上声明 @Bean 注解来从字段生成 Bean。

虽然一般情况下,这种方法应该不鼓励使用工厂方法,因为工厂方法提供了更多的灵活性,但它确实简化了测试代码。例如,使用 bean 字段,你可以在测试代码中轻松生成模拟:

import io.micronaut.context.annotation.*;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class VehicleMockSpec {
@Requires(beans = VehicleMockSpec.class)
@Bean @Replaces(Engine.class)
Engine mockEngine = () -> "Mock Started"; // (1)

@Inject Vehicle vehicle; // (2)

@Test
void testStartEngine() {
final String result = vehicle.start();
assertEquals("Mock Started", result); // (3)
}
}
  1. bean 是从替换现有 Engine 的字段中定义的。
  2. Vehicle 被注入。
  3. 代码断言调用了模拟实现。

请注意,非基元类型仅支持公共或包保护字段。如果字段是 staticprivateprotected 的,则会发生编译错误。

注意

如果bean方法/字段包含作用域或限定符,则将省略该类型中的任何作用域或限制符。

注意

工厂实例的限定符不会继承到bean

基本 bean 和数组

从 Micronaut 3.1 开始,可以从工厂定义和注入基本类型和数组类型。

例如:

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Named;

@Factory
class CylinderFactory {
@Bean
@Named("V8") // (1)
final int v8 = 8;

@Bean
@Named("V6") // (1)
final int v6 = 6;
}
  1. 使用不同的名称定义了两个基本整数 bean

基本 bean 可以像任何其他 bean 一样被注入:

import jakarta.inject.Named;
import jakarta.inject.Singleton;

@Singleton
public class V8Engine {
private final int cylinders;

public V8Engine(@Named("V8") int cylinders) { // (1)
this.cylinders = cylinders;
}

public int getCylinders() {
return cylinders;
}
}

请注意,基元 bean 和基本数组 bean 具有以下限制:

  • AOP advice 不能应用于原语或包装器类型
  • 由于上述自定义作用域,不支持代理
  • 不支持 @Bean(preDestroy=..) 成员

编程禁用 Bean

工厂方法可以抛出 DisabledBeanException 以有条件地禁用 bean。使用 @Requires 应该始终是有条件地创建 bean 的首选方法;只有在无法使用 @Requires 时,才能在工厂方法中引发异常。

例如:

public interface Engine {
Integer getCylinders();
}

@EachProperty("engines")
public class EngineConfiguration implements Toggleable {

private boolean enabled = true;
private Integer cylinders;

@NotNull
public Integer getCylinders() {
return cylinders;
}

public void setCylinders(Integer cylinders) {
this.cylinders = cylinders;
}

@Override
public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

}

@Factory
public class EngineFactory {

@EachBean(EngineConfiguration.class)
public Engine buildEngine(EngineConfiguration engineConfiguration) {
if (engineConfiguration.isEnabled()) {
return engineConfiguration::getCylinders;
} else {
throw new DisabledBeanException("Engine configuration disabled");
}
}
}

注入点

工厂的一个常见用例是从注入对象的点利用注解元数据,从而可以基于所述元数据修改行为。

考虑以下注解:

@Documented
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Cylinders {
int value() default 8;
}

上述注解可用于自定义我们希望在定义的注入点处注入到车辆中的发动机类型:

@Singleton
class Vehicle {

private final Engine engine;

Vehicle(@Cylinders(6) Engine engine) {
this.engine = engine;
}

String start() {
return engine.start();
}
}

上述 Vehicle 类指定了 @Cylinders(6) 的注解值,表示需要六个气缸的 Engine

要实现此用例,请定义一个接受 InjectionPoint 实例的工厂,以分析定义的注解值:

@Factory
class EngineFactory {

@Prototype
Engine v8Engine(InjectionPoint<?> injectionPoint, CrankShaft crankShaft) { // (1)
final int cylinders = injectionPoint
.getAnnotationMetadata()
.intValue(Cylinders.class).orElse(8); // (2)
switch (cylinders) { // (3)
case 6:
return new V6Engine(crankShaft);
case 8:
return new V8Engine(crankShaft);
default:
throw new IllegalArgumentException("Unsupported number of cylinders specified: " + cylinders);
}
}
}
  1. 工厂方法定义了 InjectionPoint 类型的参数。
  2. 注解元数据用于获取 @Cylinder 注解的值
  3. 该值用于构造引擎,如果无法构造引擎,则引发异常。
注意

需要注意的是,工厂声明为 @Prototype 作用域,因此每个注入点都会调用该方法。如果 V8EngineV6Engine 类型需要是单体的,工厂应该使用 Map 来确保对象只构造一次

3.9 条件 Bean

有时,你可能希望基于各种潜在因素,包括 classpath、配置、其他bean的存在等,有条件地加载bean。

Requires 注解提供了在 bean 上定义一个或多个条件的能力。

考虑以下示例:

使用 @Requires

@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {

DataSource dataSource;

public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}

上面的 bean 定义了两个需求。第一个指示必须存在 DataSource bean 才能加载该 bean。第二个要求确保在加载 JdbcBookService bean 之前设置 datasource.url 属性。

注意

Kotlin 目前不支持可重复注解。当需要多个需求时,使用 @Requirements 注解。例如,@Requirements(Requires(…​), Requires(…​))。参阅 https://youtrack.jetbrains.com/issue/KT-12794 以跟踪此功能。

如果多个 bean 需要相同的需求组合,则可以使用要求定义元注解:

使用 @Requires 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public @interface RequiresJdbc {
}

在上面的示例中,RequiresJdbc 注解可以在 JdbcBookService 上使用:

使用元注解

@RequiresJdbc
public class JdbcBookService implements BookService {
...
}

如果你有多个 bean 需要在加载之前满足给定的需求,那么你可能需要考虑一个 bean 配置组,如下一节所述。

配置要求

@Requires 注解非常灵活,可以用于各种用例。下表总结了一些可能性:

要求示例
要求存在一个或多个类@Requires(classes=javax.servlet.Servlet)
要求缺少一个或多个类@Requires(missing=javax.servlet.Servlet)
要求存在一个或多个 bean@Requires(beans=javax.sql.DataSource)
要求缺少一个或多个 bean@Requires(missingBeans=javax.sql.DataSource)
要求应用环境变量@Requires(env="test")
要求不应用环境变量@Requires(notEnv="test")
要求另一个配置包@Requires(configuration="foo.bar")
要求缺少另一个配置包@Requires(missingConfigurations="foo.bar")
要求特定 SDK 版本@Requires(sdk=Sdk.JAVA, value="1.8")
要求通过包扫描向应用程序提供带有给定注解的类@Requires(entities=javax.persistence.Entity)
要求具有可选值的属性@Requires(property="data-source.url")
要求属性不是配置的一部分@Requires(missingProperty="data-source.url")
要求文件系统中存在一个或多个文件@Requires(resources="file:/path/to/file")
要求存在一个或多个 classpath 资源@Requires(resources="classpath:myFile.properties")
要求当前操作系统在列表中@Requires(os={Requires.Family.WINDOWS})
要求当前操作系统在列表中@Requires(notOs={Requires.Family.WINDOWS})
如果未指定 beanProperty,则要求 bean@Requires(bean=Config.class)
要求存在 bean 的指定属性@Requires(bean=Config.class, beanProperty="enabled")

属性要求附加说明。

在属性上添加需求具有一些附加功能。你可以要求属性为特定值,而不是特定值,如果未设置,则在这些检查中使用默认值。

@Requires(property="foo") (1)
@Requires(property="foo", value="John") (2)
@Requires(property="foo", value="John", defaultValue="John") (3)
@Requires(property="foo", notEquals="Sally") (4)
  1. 需要设置属性
  2. 要求属性为 "John"
  3. 要求属性为 "John" 或未设置
  4. 要求属性不为 "Sally" 或未设置

在 @Requires 中引用 bean 属性

你还可以在 @Requires 中引用其他 bean 属性,以有条件地加载 bean。与 property 要求类似,你可以指定所需的 value 或设置值 bean 属性不应等于使用 notEquals 注解成员。对于要检查的 bean 属性,bean 注解成员中指定的 bean 类型应该存在于上下文中,否则将不会加载条件 bean。

@Requires(bean=Config.class, beanProperty="foo") (1)
@Requires(bean=Config.class, beanProperty="foo", value="John") (2)
@Requires(bean=Config.class, beanProperty="foo", notEquals="Sally") (3)
  1. 要求 Config bean 上的 "foo" 属性
  2. 要求 Config bean 上的 "foo" 属性为 "John"
  3. 要求 Config bean 上的 "foo" 属性不为 "Sally" 或不设置

指定的 bean 属性通过相应的 getter 方法访问,其存在性和可用性将在编译时检查。

注意,如果 bean 属性的值不为 null,则认为它存在。请记住,基本属性是用默认值初始化的,例如布尔值为 false,int 值为 0,因此即使没有为它们显式指定值,也会将它们视为已设置。

调试条件 Bean

如果你有多个条件和复杂的需求,那么可能很难理解为什么没有加载特定的 bean。

为了帮助解决条件 bean 的问题,你可以为 io.micronaut.context.condition 包启用调试日志记录,该包将记录未加载 bean 的原因。

logback.xml

<logger name="io.micronaut.context.condition" level="DEBUG"/>

有关如何设置日志的详细信息,参阅日志一章。

3.10 Bean 替换

Micronaut 的依赖注入系统和 Spring 的一个显著区别是 bean 的替换方式。

在 Spring 应用程序中,bean 具有名称,并通过创建具有相同名称的 bean 来覆盖,而不考虑 bean 的类型。Spring 还具有 bean 注册顺序的概念,因此在 Spring Boot 中有 @AutoConfigureBefore@AutoConfigureAfter 注解,它们控制 bean 如何相互覆盖。

此策略会导致难以调试的问题,例如:

  • Bean 加载顺序更改,导致意外结果
  • 具有相同名称的bean覆盖具有不同类型的另一个bean

为了避免这些问题,Micronaut 的 DI 没有 bean 名称或加载顺序的概念。bean 有一种类型和 Qualifier。不能用另一个完全不同类型的 bean 重写。

Spring 方法的一个有用的好处是它允许重写现有 bean 来定制行为。为了支持相同的功能,Micronaut 的 DI 提供了一个显式的 @Replaces 注解,它与对条件 Bean的支持很好地集成在一起,并清晰地记录和表达了开发人员的意图。

任何现有的 bean 都可以被声明 @Replaces 的另一个 bean 替换。例如,考虑以下类:

JdbcBookService

@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {

DataSource dataSource;

public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}

你可以在 src/test/java 中定义一个类,该类仅用于测试:

使用 @Replaces

@Replaces(JdbcBookService.class) // (1)
@Singleton
public class MockBookService implements BookService {

Map<String, Book> bookMap = new LinkedHashMap<>();

@Override
public Book findBook(String title) {
return bookMap.get(title);
}
}
  1. MockBookService 声明它将替换 JdbcBookService

工厂替换

@Replaces 注解还支持 factory 参数。该参数允许替换整个工厂 bean 或工厂创建的特定类型。

例如,可能需要替换所有或部分给定工厂类别:

BookFactory

@Factory
public class BookFactory {

@Singleton
Book novel() {
return new Book("A Great Novel");
}

@Singleton
TextBook textBook() {
return new TextBook("Learning 101");
}
}
警告

要完全替换工厂,工厂方法必须与替换工厂中所有方法的返回类型匹配。

在本例中,BookFactory#textBook() 被替换,因为该工厂没有返回 TextBook 的工厂方法。

CustomBookFactory

@Factory
@Replaces(factory = BookFactory.class)
public class CustomBookFactory {

@Singleton
Book otherNovel() {
return new Book("An OK Novel");
}
}

要替换一个或多个工厂方法,但保留其余方法,请在方法上应用 @Replaces 注解,并表示要应用的工厂。

TextBookFactory

@Factory
public class TextBookFactory {

@Singleton
@Replaces(value = TextBook.class, factory = BookFactory.class)
TextBook textBook() {
return new TextBook("Learning 305");
}
}

BookFactory#novel() 方法不会被替换,因为 TextBook 类是在注解中定义的。

默认实现

在公开 API 时,最好不要将接口的默认实现公开为公共 API 的一部分。这样做会阻止用户替换实现,因为他们将无法引用类。解决方案是用 DefaultImplementation 注解接口,以指示如果用户创建了 @Replaces(YourInterface.class 的bean,则要替换哪个实现。

例如,考虑:

public API 约定

import io.micronaut.context.annotation.DefaultImplementation;

@DefaultImplementation(DefaultResponseStrategy.class)
public interface ResponseStrategy {
}

默认实现

import jakarta.inject.Singleton;

@Singleton
class DefaultResponseStrategy implements ResponseStrategy {

}

自定义实现

import io.micronaut.context.annotation.Replaces;
import jakarta.inject.Singleton;

@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {

}

在上面的示例中,CustomResponseStrategy 替换了 DefaultResponsePolicy,因为 DefaultImplementation 注解指向 DefaultResponceStrategy

3.11 Bean 配置

一个带 @Configuration 的 bean 是包中多个 bean 定义的分组。

@Configuration 注解应用于包级别,并通知 Micronaut 与包一起定义的 bean 形成了一个逻辑分组。

@Configuration 注解通常应用于 package-info 类。例如:

package-info.groovy

@Configuration
package my.package

import io.micronaut.context.annotation.Configuration

当 bean 配置通过 @Requires 注解设置为有条件时,这种分组变得有用。例如:

package-info.groovy

@Configuration
@Requires(beans = javax.sql.DataSource)
package my.package

在上面的示例中,只有当 javax.sql.DataSource bean 存在时,才会加载带注解包中的所有 bean 定义并使其可用。这允许你实现 bean 定义的条件自动配置。

注意

Java 和 Kotlin 也通过 package-info.java 支持此功能。Kotlin 不支持 1.3 版的 package-ininfo.kt

3.12 生命周期方法

当构建 Bean 时

要在构建 bean 时调用方法,请使用 jakarta.annotation.PostConstruct 注解:

import jakarta.annotation.PostConstruct; // (1)
import jakarta.inject.Singleton;

@Singleton
public class V8Engine implements Engine {

private int cylinders = 8;
private boolean initialized = false; // (2)

@Override
public String start() {
if (!initialized) {
throw new IllegalStateException("Engine not initialized!");
}

return "Starting V8";
}

@Override
public int getCylinders() {
return cylinders;
}

public boolean isInitialized() {
return initialized;
}

@PostConstruct // (3)
public void initialize() {
initialized = true;
}
}
  1. PostConstruct 注解已导入
  2. 定义了需要初始化的字段
  3. 一个方法用 @PostConstruct 注解,一旦对象被构造并完全注入,就会被调用。

要管理何时构建 bean,请参阅 bean 作用域一节。

当销毁 Bean 时

要在销毁 bean 时调用方法,请使用 jakarta.annotation.PreDestroy 注解:

import jakarta.annotation.PreDestroy; // (1)
import jakarta.inject.Singleton;
import java.util.concurrent.atomic.AtomicBoolean;

@Singleton
public class PreDestroyBean implements AutoCloseable {

AtomicBoolean stopped = new AtomicBoolean(false);

@PreDestroy // (2)
@Override
public void close() throws Exception {
stopped.compareAndSet(false, true);
}
}
  1. 导入 PreDestroy 注解
  2. 方法用 @PreDestroy 注解,并将在上下文关闭时调用。

对于工厂 Bean,Bean 注解中的 preDestroy 值告诉 Micronaut 要调用哪个方法。

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;

import jakarta.inject.Singleton;

@Factory
public class ConnectionFactory {

@Bean(preDestroy = "stop") // (1)
@Singleton
public Connection connection() {
return new Connection();
}
}
import java.util.concurrent.atomic.AtomicBoolean;

public class Connection {

AtomicBoolean stopped = new AtomicBoolean(false);

public void stop() { // (2)
stopped.compareAndSet(false, true);
}

}
  1. preDestroy 值设置在注解上
  2. 注解值与方法名称匹配
注意

简单地实现 CloseableAutoCloseable 接口不足以使 bean 与上下文一起关闭。必须使用上述方法之一。

依赖 Bean

依赖 bean 是构建 bean 时使用的 bean。如果依赖 bean 的作用域为 @Prototype 或未知,它将与实例一起销毁。

3.13 上下文事件

Micronaut 通过上下文支持通用事件系统。ApplicationEventPublisher API 发布事件,ApplicationEventListener API 用于侦听事件。事件系统不限于 Micronaut 发布并支持用户创建的自定义事件。

发布事件

ApplicationEventPublisher API 支持任何类型的事件,尽管 Micronaut 发布的所有事件都继承 ApplicationEvent。

要发布事件,请使用依赖注入获取 ApplicationEventPublisher 的实例,其中泛型类型是事件的类型,并使用事件对象调用 publishEvent 方法。

发布事件

public class SampleEvent {
private String message = "Something happened";

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

import io.micronaut.context.event.ApplicationEventPublisher;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

@Singleton
public class SampleEventEmitterBean {

@Inject
ApplicationEventPublisher<SampleEvent> eventPublisher;

public void publishSampleEvent() {
eventPublisher.publishEvent(new SampleEvent());
}

}
警告

默认情况下,发布事件是同步的!在执行所有侦听器之前,publishEvent 方法不会返回。如果时间密集,将此工作移到线程池。

监听事件

要侦听事件,请注册一个实现 ApplicationEventListener 的 bean,其中泛型类型是事件类型。

使用 ApplicationEventListener 侦听事件

import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.docs.context.events.SampleEvent;
import jakarta.inject.Singleton;

@Singleton
public class SampleEventListener implements ApplicationEventListener<SampleEvent> {
private int invocationCounter = 0;

@Override
public void onApplicationEvent(SampleEvent event) {
invocationCounter++;
}

public int getInvocationCounter() {
return invocationCounter;
}
}

import io.micronaut.context.ApplicationContext;
import io.micronaut.docs.context.events.SampleEventEmitterBean;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class SampleEventListenerSpec {

@Test
public void testEventListenerIsNotified() {
try (ApplicationContext context = ApplicationContext.run()) {
SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean.class);
SampleEventListener listener = context.getBean(SampleEventListener.class);
assertEquals(0, listener.getInvocationCounter());
emitter.publishSampleEvent();
assertEquals(1, listener.getInvocationCounter());
}
}
}
注意

可以重写 supports 方法以进一步澄清要处理的事件。

或者,如果你不希望实现接口或使用内置事件之一,如 StartupEventShutdownEvent,请使用 @EventListener 注解:

使用 @EventListener 监听事件

import io.micronaut.docs.context.events.SampleEvent;
import io.micronaut.context.event.StartupEvent;
import io.micronaut.context.event.ShutdownEvent;
import io.micronaut.runtime.event.annotation.EventListener;

@Singleton
public class SampleEventListener {
private int invocationCounter = 0;

@EventListener
public void onSampleEvent(SampleEvent event) {
invocationCounter++;
}

@EventListener
public void onStartupEvent(StartupEvent event) {
// startup logic here
}

@EventListener
public void onShutdownEvent(ShutdownEvent event) {
// shutdown logic here
}

public int getInvocationCounter() {
return invocationCounter;
}
}

如果侦听器执行的工作可能需要一段时间,请使用 @Async 注解在单独的线程上运行该操作:

使用 @EventListener 异步监听事件