跳到主要内容

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 异步监听事件


默认情况下,事件侦听器在计划的执行器上运行。你可以在 application.yml 中根据需要配置此线程池:

配置计划任务线程池

micronaut:
executors:
scheduled:
type: scheduled
core-pool-size: 30

3.14 Bean 事件

你可以使用以下接口之一钩住 bean 的创建:

BeanInitializedEventListener 接口通常与 Factory bean 结合使用。考虑以下示例:

public class V8Engine implements Engine {
private final int cylinders = 8;
private double rodLength; // (1)

public V8Engine(double rodLength) {
this.rodLength = rodLength;
}

@Override
public String start() {
return "Starting V" + getCylinders() + " [rodLength=" + getRodLength() + ']';
}

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

public double getRodLength() {
return rodLength;
}

public void setRodLength(double rodLength) {
this.rodLength = rodLength;
}
}

@Factory
public class EngineFactory {

private V8Engine engine;
private double rodLength = 5.7;

@PostConstruct
public void initialize() {
engine = new V8Engine(rodLength); // (2)
}

@Singleton
public Engine v8Engine() {
return engine;// (3)
}

public void setRodLength(double rodLength) {
this.rodLength = rodLength;
}
}

@Singleton
public class EngineInitializer implements BeanInitializedEventListener<EngineFactory> { // (4)
@Override
public EngineFactory onInitialized(BeanInitializingEvent<EngineFactory> event) {
EngineFactory engineFactory = event.getBean();
engineFactory.setRodLength(6.6);// (5)
return engineFactory;
}
}
  1. V8Engine 类定义了 rodLength 属性
  2. EngineFactory 初始化 rodLength 的值并创建实例
  3. 创建的实例作为 Bean 返回
  4. 实现 BeanInitializedEventListener 接口以监听工厂的初始化
  5. onInitialized 方法中,rodLength 在工厂 bean 创建引擎之前被重写。

BeanCreatedEventListener 接口通常用于修饰或增强完全初始化的 bean,例如通过创建代理。

危险

Bean 事件监听器在类型转换器初始化。如果事件监听器通过依赖配置属性 bean 或任何其他机制依赖类型转换,则可能会看到与类型转换相关的错误。

3.15 Bean 自省

从 Micronaut 1.1 开始,JDK 的 Introspector 类就包含了编译时替换。

BeanIntrospectorBeanIntrospection 接口允许查找 bean 自省来实例化和读/写 bean 属性,而无需使用反射或缓存反射元数据,因为反射元数据会为大型 bean 消耗过多内存。

让 Bean 可供自省

与 JDK 的 Introspector 不同,不是每个类都可以自动进行自省。要使一个类可用于自省,你必须在构建中至少启用 Micronaut 的注解处理器(micronaut-inject-java 用于 Java 和 Kotlin,micronaut-inject-groovy 用于 Groovy),并确保有依赖 micronaut-core 的运行时。

annotationProcessor("io.micronaut:micronaut-inject-java:3.9.4")
注意

对于 Kotlin,在 kapt 范围中添加 micronaut-inject-java 依赖,对于 Groovy,在 compileOnly 范围中添加 micronaut-inject-groovy

runtimeOnly("io.micronaut:micronaut-core:3.9.4")

使用 @Introspected 注解

@Introspected 注解可用于任何类,以使其可用于自省。简单使用 @Introspected 注解类:

import io.micronaut.core.annotation.Introspected;

@Introspected
public class Person {

private String name;
private int age = 18;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

一旦在编译时生成了自省数据,就可以通过 BeanTranspection API 检索它:

final BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class); // (1)
Person person = introspection.instantiate("John"); // (2)
System.out.println("Hello " + person.getName());

final BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String.class); // (3)
property.set(person, "Fred"); // (4)
String name = property.get(person); // (5)
System.out.println("Hello " + person.getName());
  1. 你可以使用静态 getIntrospection 方法检索 BeanIntrospection
  2. 一旦有了 BeanIntrospection,就可以使用 instantiate 方法实例化一个 bean。
  3. 可以从自省中检索 BeanProperty
  4. 使用 set 方法设置属性值
  5. 使用 get 方法检索属性值

@Introspected 和 @AccessorsStyle 共同使用

可以将 @AccessorsStyle 注解与 @Introspected 一起使用:

import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.annotation.Introspected;

@Introspected
@AccessorsStyle(readPrefixes = "", writePrefixes = "") (1)
public class Person {

private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String name() { (2)
return name;
}

public void name(String name) { (2)
this.name = name;
}

public int age() { (2)
return age;
}

public void age(int age) { (2)
this.age = age;
}
}
  1. @AccessorsStyle 注解类,为 getter 和 setter 定义空的读写前缀。
  2. 定义不带前缀的 getter 和 setter。

现在,可以使用 BeanTurpection API 检索编译时生成的自省:

BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class);
Person person = introspection.instantiate("John", 42);

Assertions.assertEquals("John", person.name());
Assertions.assertEquals(42, person.age());

Bean 字段

默认情况下,Java 自省仅将 JavaBean getter/setter 或 Java 16 记录组件视为 bean 属性。但是,你可以使用 @Introspected 注解的 accessKind 成员在 Java 中定义带有公共或包保护字段的类:

import io.micronaut.core.annotation.Introspected;

@Introspected(accessKind = Introspected.AccessKind.FIELD)
public class User {
public final String name; // (1)
public int age = 18; // (2)

public User(String name) {
this.name = name;
}
}
  1. final 字段被视为只读属性
  2. 可变字段被视为读写属性
注意

accessKind 接受一个数组,因此可以允许两种类型的访问器,但根据它们在注解中出现的顺序,更喜欢其中一种。列表中的第一个具有优先级。

危险

在 Kotlin 中无法对字段进行自省,因为无法直接声明字段。

构造方法

对于具有多个构造函数的类,将 @Creator 注解应用于要使用的构造函数。

import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.Introspected;

import javax.annotation.concurrent.Immutable;

@Introspected
@Immutable
public class Vehicle {

private final String make;
private final String model;
private final int axles;

public Vehicle(String make, String model) {
this(make, model, 2);
}

@Creator // (1)
public Vehicle(String make, String model, int axles) {
this.make = make;
this.model = model;
this.axles = axles;
}

public String getMake() {
return make;
}

public String getModel() {
return model;
}

public int getAxles() {
return axles;
}
}
  1. @Creator 注解表示要使用的构造函数
注意

该类没有默认构造函数,因此在没有参数的情况下调用实例化会引发 InstantiationException

静态 Creator 方法

@Creator 注解可以应用于创建类实例的静态方法。

import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.Introspected;

import javax.annotation.concurrent.Immutable;

@Introspected
@Immutable
public class Business {

private final String name;

private Business(String name) {
this.name = name;
}

@Creator // (1)
public static Business forName(String name) {
return new Business(name);
}

public String getName() {
return name;
}
}
  1. @Creator 注解应用于实例化类的静态方法
提示

可以注解多个“creator”方法。如果有一个没有参数,它将是默认的构造方法。第一个带参数的方法将用作主要构造方法。

枚举

也可以对枚举进行自省。将注解添加到枚举中,它可以通过标准 valueOf 方法构造。

在配置类中使用 @Introspected

如果要自省的类已经编译并且不在你的控制之下,另一种选择是使用 @Introspected 注解集的 classes 成员定义一个配置类。

import io.micronaut.core.annotation.Introspected;

@Introspected(classes = Person.class)
public class PersonConfiguration {
}

在上面的示例中,PersonConfiguration 类为 Person 类生成自省。

注意

你还可以使用 @Introspectedpackages 成员,该包在编译时扫描并为包中的所有类生成自省。注意,此功能目前被视为实验性。

编写 AnnotationMapper 以自省现有注解

如果默认情况下你希望自省现有注解,则可以编写 AnnotationMap

这方面的一个例子是 EntityIntrospectedAnnotationMap,它确保所有用 javax.persistence.Entity 注解的 bean 在默认情况下都是可自省的。

注意

AnnotationMap 必须位于注解处理器 classpath 上。

BeanWrapper API

BeanProperty 提供了读取和写入给定类的属性值的原始访问,不提供任何自动类型转换。

传递给 setget 方法的值应与基础属性类型匹配,否则将发生异常。

为了提供额外的类型转换智能,BeanWrapper 接口允许包装现有 bean 实例,设置并获取 bean 的属性,并根据需要执行类型转换。

final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")); // (1)

wrapper.setProperty("age", "20"); // (2)
int newAge = wrapper.getRequiredProperty("age", int.class); // (3)

System.out.println("Person's age now " + newAge);
  1. 使用静态 getWrapper 方法获取 bean 实例的 BeanWrapper
  2. 你可以设置财产,BeanWrapper 将执行类型转换,如果转换不可能,则抛出 ConversionErrorException
  3. 你可以使用 getRequiredProperty 检索属性并请求适当的类型。如果属性不存在,则引发 IntrospectionException,如果无法转换,则引发 ConversionErrorException

Jackson 与 Bean 自省

Jackson 被配置为使用 BeanIntrospection API 来读写属性值和构造对象,从而实现无反射的序列化/反序列化。从性能角度来看,这是有益的,并且需要较少的配置才能在运行时(如 GraalVM 本地)中正确运行。

默认情况下启用此功能;通过将 jackson.bean-introspection-module 配置设置为 false 来禁用它。

注意

目前只支持 bean 属性(带有公共 getter/setter 的私有字段),不支持使用公共字段。

注意

该功能目前处于试验阶段,将来可能会发生变化。

3.16 Bean 校验

从 Micronaut 1.2 开始,Micronaut 就内置了对用 javax.validation 注解的 bean 进行验证的支持。至少应将 micronaut-validation 模块作为编译依赖项:

implementation("io.micronaut:micronaut-validation")

注意,Micronaut 的实现目前不完全符合 Bean Validator 规范,因为该规范严重依赖于基于反射的 API。

目前不支持以下功能:

  • 泛型参数类型上的注解,因为只有 Java 语言支持此功能。
  • 约束元数据 API 的任何交互,因为 Micronaut 使用编译时生成的元数据。
  • 基于 XML 的配置

不要使用 javax.validation.ConstraintValidator,而是使用 ConstraintValidator(io.minout.validation.validator.constraints.ConstraintPValidator)定义自定义约束,该约束支持在编译时验证注解。

Micronaut 的实现包括以下好处:

  • 反射和运行时无代理验证,减少了内存消耗
  • 由于 Hibernate Validator 又增加了 1.4MB,JAR 的大小更小
  • 由于 Hibernate Validator 增加了 200ms 以上的启动开销,因此启动速度更快
  • 可通过注解元数据进行配置
  • 支持 Reactive Bean 验证
  • 支持在编译时验证源 AST
  • 与 GraalVM 本地自动兼容,无需额外配置

如果你需要完全符合 Bean Validator 2.0,请将 micronaut-hibernate-validator 模块添加到你的构建中,以替代 Micronaut 的实现。

implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")

校验 Bean 方法

通过对参数应用 javax.validation 注解,可以验证任何声明为 Micronaut bean 的类的方法:

校验方法

import jakarta.inject.Singleton;
import javax.validation.constraints.NotBlank;

@Singleton
public class PersonService {
public void sayHello(@NotBlank String name) {
System.out.println("Hello " + name);
}
}

上面的示例声明调用 sayHello 方法时将验证 @NotBlank 注解。

警告

如果使用 Kotlin,则必须将类和方法声明为 open 的,这样 Micronaut 才能创建编译时子类。或者,你可以使用 @Validated 注解类,并将 Kotlin all-open 插件配置为使用此类型注解的类。参阅编译器插件部分。

如果发生验证错误,将引发 javax.validation.ConstraintViolationException 。例如:

ConstraintViolationException 示例

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;
import javax.validation.ConstraintViolationException;

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

@MicronautTest
class PersonServiceSpec {

@Inject PersonService personService;

@Test
void testThatNameIsValidated() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
personService.sayHello("") // (1)
);

assertEquals("sayHello.name: must not be blank", exception.getMessage()); // (2)
}
}
  1. 方法使用空串调用
  2. 异常出现

校验数据类

要验证数据类,例如 POJO(通常用于 JSON 交换),必须用 @Introspected 注解该类(参阅前面关于 Bean 自省 的章节),或者,如果该类是外部的,则通过 @Introsspected 注解导入。

POJO 校验示例

import io.micronaut.core.annotation.Introspected;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;

@Introspected
public class Person {

private String name;

@Min(18)
private int age;

@NotBlank
public String getName() {
return name;
}

public int getAge() {
return age;
}

public void setName(String name) {
this.name = name;
}

public void setAge(int age) {
this.age = age;
}
}
提示

@Introspected 注解可以用作元注解;像 @javax.persistence.Entity 这样的常见注解被视为 @Introspected

上面的示例定义了一个 Person 类,该类有两个应用了约束的属性(nameage)。注意,在 Java 中,注解可以位于字段或 getter 上,对于 Kotlin 数据类,注解应该以字段为目标。

要手动验证类,请注入 Validator 的实例:

手动校验示例

@Inject
Validator validator;

@Test
void testThatPersonIsValidWithValidator() {
Person person = new Person();
person.setName("");
person.setAge(10);

final Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person); // (1)

assertEquals(2, constraintViolations.size()); // (2)
}
  1. 验证器验证 person
  2. 验证约束冲突

或者,在 Bean 方法上,你可以使用 javax.validation.Valid 来触发级联验证:

ConstraintViolationException 示例

@Singleton
public class PersonService {
public void sayHello(@Valid Person person) {
System.out.println("Hello " + person.getName());
}
}
  1. 已调用验证的方法
  2. 验证约束冲突

校验配置属性

你还可以验证用 @ConfigurationProperties 注解的类的属性,以确保配置正确。

注意

建议你使用 @Context 注解具有验证功能的 @ConfigurationProperties,以确保在启动时进行验证

定义额外约束

要定义额外约束,请创建新注解,例如:

约束注解示例

import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Constraint(validatedBy = { }) // (1)
public @interface DurationPattern {

String message() default "invalid duration ({validatedValue})"; // (2)

/**
* Defines several constraints on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
DurationPattern[] value(); // (3)
}
}
  1. 注解应使用 javax.validationConstraint 进行注解
  2. 可以按如上所述的硬编码方式提供 message 模板。如果未指定,Micronaut 将尝试使用 MessageSource 接口(可选)使用 ClassName.message 查找 message
  3. 为了支持重复注解,可以定义内部注解(可选)
提示

可以使用 MessageSourceResourceBundleMessageSource 类添加消息和消息束。参阅资源捆绑包文档。

定义注解后,请实现一个用于验证注解的 ConstraintValidator。你可以创建一个直接实现接口的 bean 类,也可以定义一个返回一个或多个验证器的工厂。

如果你计划定义多个验证器,建议使用后一种方法:

约束校验器示例

import io.micronaut.context.annotation.Factory;
import io.micronaut.validation.validator.constraints.ConstraintValidator;

import jakarta.inject.Singleton;

@Factory
public class MyValidatorFactory {

@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return (value, annotationMetadata, context) -> {
context.messageTemplate("invalid duration ({validatedValue}), additional custom message"); // (1)
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
};
}
}
  1. 使用内联调用重写默认消息模板,以获得对验证错误消息的更多控制。(自 2.5.0 起)

上面的示例实现了一个验证器,它验证用 DurationPattern 注解的任何字段、参数等,确保可以使用 java.time.Duration.parse 解析字符串。

注意

通常,null 被视为有效,@NotNull 用于约束值不为 null。上面的示例将 null 视为有效值。

例如:

自定义约束使用示例

@Singleton
public class HolidayService {

public String startHoliday(@NotBlank String person,
@DurationPattern String duration) {
final Duration d = Duration.parse(duration);
return "Person " + person + " is off on holiday for " + d.toMinutes() + " minutes";
}

public String startHoliday(@DurationPattern String fromDuration, @DurationPattern String toDuration, @NotBlank String person
) {
final Duration d = Duration.parse(fromDuration);
final Duration e = Duration.parse(toDuration);
return "Person " + person + " is off on holiday from " + d + " to " + e;
}
}

要验证上述示例是否验证 duration 参数,请定义测试:

自定义约束使用测试示例

@Inject HolidayService holidayService;

@Test
void testCustomValidator() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
holidayService.startHoliday("Fred", "junk") // (1)
);

assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.getMessage()); // (2)
}

// Issue:: micronaut-core/issues/6519
@Test
void testCustomAndDefaultValidator() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
holidayService.startHoliday( "fromDurationJunk", "toDurationJunk", "")
);

String notBlankValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.person")).map(ConstraintViolation::getMessage).findFirst().get();
String fromDurationPatternValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.fromDuration")).map(ConstraintViolation::getMessage).findFirst().get();
String toDurationPatternValidated = exception.getConstraintViolations().stream().filter(constraintViolation -> Objects.equals(constraintViolation.getPropertyPath().toString(), "startHoliday.toDuration")).map(ConstraintViolation::getMessage).findFirst().get();
assertEquals("must not be blank", notBlankValidated);
assertEquals("invalid duration (fromDurationJunk), additional custom message", fromDurationPatternValidated);
assertEquals("invalid duration (toDurationJunk), additional custom message", toDurationPatternValidated);
}
  1. 已调用验证的方法
  2. 验证了约束冲突

编译时校验注解

你可以使用 Micronaut 的验证器在编译时验证注解元素,方法是在注解处理器 classpath 中包含 micronaut-validation

annotationProcessor("io.micronaut:micronaut-validation")

然后,Micronaut 将在编译时验证自己用 javax.validation 注解的注解值。例如,考虑以下注解:

注解校验

import java.lang.annotation.Retention;

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

@Retention(RUNTIME)
public @interface TimeOff {
@DurationPattern
String duration();
}

如果你尝试在源代码中使用 @TimeOff(duration="junk"),Micronaut 将因 duration 值违反 DurationPattern 约束而导致编译失败。

注意

如果 duration 是一个属性占位符,例如 @TimeOff(duration="${my.value}"),则验证将延迟到运行时。

请注意,要在编译时使用自定义 ConstraintValidator,必须将验证器定义为类:

约束校验器示例

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;

public class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
public boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
}
}

此外:

  1. 定义引用类的 META-INF/service/io.micronaut.validation.validator.constraints.ConstraintValidator 文件。
  2. 类必须是公共的,并且具有公共的无参数构造函数
  3. 该类必须位于要验证的项目的注解处理器 classpath

3.17 Bean 注解元数据

Java 的 AnnotatedElement API 提供的方法通常不提供在不加载注解本身的情况下自省注解的能力。它们也不提供任何自省注解原型的能力(通常称为元注解;注解原型是用另一个注解来注解注解,本质上继承其行为)。

为了解决这个问题,许多框架都会生成运行时元数据或执行昂贵的反射来分析类的注解。

相反,Micronaut 在编译时生成这个注解元数据,避免了昂贵的反射并节省了内存。

BeanContext API 可用于获取对实现 AnnotationMetadata 接口的 BeanDefinition 的引用。

例如,下面的代码获得了用特定原型注解的所有 bean 定义:

按结构类型查找 Bean 定义

BeanContext beanContext = ... // obtain the bean context
Collection<BeanDefinition> definitions =
beanContext.getBeanDefinitions(Qualifiers.byStereotype(Controller.class))

for (BeanDefinition definition : definitions) {
AnnotationValue<Controller> controllerAnn = definition.getAnnotation(Controller.class);
// do something with the annotation
}

上面的示例找到了所有用 @Controller 注解的 BeanDefinition 实例,无论 @Controller 是直接使用还是通过注解原型继承。

注意,getAnnotation 方法及其变体返回 AnnotationValue 类型,而不是 Java 注解。这是设计的,在读取注解值时通常应该尝试使用此 API,因为从性能和内存消耗的角度来看,合成代理实现更差。

如果需要对注解实例的引用,则可以使用 synthesize 方法,该方法创建实现注解接口的运行时代理:

合成注解实例

Controller controllerAnn = definition.synthesize(Controller.class);

但是,不建议使用这种方法,因为它需要反射,并且由于使用运行时生成的代理而增加了内存消耗,因此应作为最后的手段使用,例如,如果你需要注解的实例与第三方库集成。

注解继承

Micronaut 将遵守 Java 的 AnnotatedElement API 中定义的关于注解继承的规则:

  • 通过 AnnotationMetadata API 的 getAnnotation* 方法可以使用带有 Inherited 的注解,而直接声明的注解可以通过 getDeclaredAnnotation* 方法使用。
  • 未使用 Inherited 进行元注解的注解将不会包含在元数据中

Micronaut 与 AnnotatedElement API 的不同之处在于,它将这些规则扩展到方法和方法参数,从而:

通常情况下,你可能希望覆盖的行为不会默认继承,包括 Bean 作用域Bean 限定符Bean 条件验证规则等。

如果你希望在子类化时继承特定范围、限定符或一组要求,那么可以定义一个用 @inherited 注解的元注解。例如:

定义继承元数据

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.AnnotationMetadata;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Inherited // (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
public @interface SqlRepository {
@AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
String value() default "";
}
  1. 注解声明为 @Inherited
  2. Bean 条件 将由子类继承
  3. Bean 限定符 将由子类继承
  4. Bean 作用域 将由子类继承
  5. 你还可以别名注解,它们将被继承

有了这个元注解,你可以将注解添加到超类中:

在超类中使用继承元注解

@SqlRepository
public abstract class BaseSqlRepository {
}

然后,子类将继承所有注解:

在子类中继承注解

import jakarta.inject.Named;
import javax.sql.DataSource;

@Named("bookRepository")
public class BookRepository extends BaseSqlRepository {
private final DataSource dataSource;

public BookRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
}
注意

子类必须至少有一个 bean 定义注解,如作用域或限定符

别名/映射注解

有时,你可能希望将注解成员的值别名为另一个注解成员的数值。为此,请使用 @AliasFor 注解。

例如,一个常见的用例是注解定义了 value() 成员,但也支持其他成员。例如 @Client 注解:

@Client 注解

public @interface Client {

/**
* @return The URL or service ID of the remote service
*/
@AliasFor(member = "id") (1)
String value() default "";

/**
* @return The ID of the client
*/
@AliasFor(member = "value") (2)
String id() default "";
}
  1. value 成员也设置 id 成员
  2. id 成员也设置 value 成员

有了这些别名,无论你是定义 @Client("foo")@Client(id="foo"),都将设置 valueid 成员,从而更容易解析和处理注解。

如果无法控制注解,另一种方法是使用 AnnotationMapper。要创建 AnnotationMapper,请执行以下操作:

  • 实现 AnnotationMapper 接口
  • 定义引用实现类的 META-INF/service/io.micronaut.inject.annotation.AnnotationMapper 文件
  • 将包含实现的 JAR 文件添加到 annotationProcessor classpath(用于 Kotlin 的 kapt
注意

由于 AnnotationMapper 实现必须位于注解处理器 classpath 上,因此它们通常应位于包含很少外部依赖项的项目中,以避免污染注解处理器类。

下面是一个示例 AnnotationMapper,它改进了 JPA 实体的自省功能。

EntityIntrospectedAnnotationMapper 映射示例

public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper {
@NonNull
@Override
public String getName() {
return "javax.persistence.Entity";
}

@Override
public List<AnnotationValue<?>> map(AnnotationValue<Annotation> annotation, VisitorContext visitorContext) { (1)
final AnnotationValueBuilder<Introspected> builder = AnnotationValue.builder(Introspected.class)
// don't bother with transients properties
.member("excludedAnnotations", "javax.persistence.Transient"); (2)
return Arrays.asList(
builder.build(),
AnnotationValue.builder(ReflectiveAccess.class).build()
);
}
}
  1. map 方法接收带有注解值的 AnnotationValue
  2. 可以返回一个或多个注解,在本例中为 @Transient
注意

上面的示例实现了 NamedAnnotationMapper 接口,该接口允许注解与运行时代码混合。要针对具体的注解类型进行操作,请改用 TypedAnnotationMapper,尽管注意它需要注解类本身位于注解处理器 classpath。

3.18 从库导入 Bean

你可以使用 @Import 注解从使用 JSR-330 注解的外部已编译库中导入 bean。

注意

Bean 导入目前仅在 Java 语言中受支持,因为其他语言在源代码处理期间对 classpath 扫描有限制。

例如,要将 JSR-330 TCK 导入应用程序,请添加对 TCK 的依赖:

implementation("io.micronaut:jakarta.inject")

然后在 Application 类上定义 @Import 注解:

package example;

import io.micronaut.context.annotation.Import;

@Import( (1)
packages = { (2)
"org.atinject.tck.auto",
"org.atinject.tck.auto.accessories"},
annotated = "*") (3)
public class Application {
}
  1. 定义 @Import
  2. 定义了要导入的包。请注意,Micronaut 不会通过子包递归,因此需要显式列出子包
  3. 默认情况下,Micronaut 将仅导入具有作用域或限定符的类。通过使用 *,你可以使每种类型都成为 bean。
注意

通常,@Import 应该在应用程序中使用,而不是在库中使用,因为如果两个库导入相同的 bean,结果很可能是 NonUniqueBeaException

3.19 可为 null 注解

在 Java 中,可以使用注解来显示变量是否可以为 null。这些注解不是标准库的一部分,但你可以单独添加它们。

Micronaut 框架自带一组注解来声明可为空 @Nullable@NonNull

为什么 Micronaut 框架会添加自己的一组可空性注解,而不是使用现有的一个可为 null 注解库?

在整个框架的历史中,我们使用了其他可为 null 的注解库。然而,许可问题使我们多次更改了可为 null 的注解。为了避免将来必须更改可为 null 的注解,我们在 Micronaut Framework 2.4 中添加了自己的一组可为 null 注解。

Kotlin 是否认可 Micronaut 可为 null 注解?

是的,Micronaut 可为 null 注解在编译时映射到 javax.annotation.Nullablejavax.anneration.Nonnull

为什么要在代码中使用可为 null 注解?

它使你的代码更容易从 Kotlin 使用。当你从 Kotlin 代码调用 Java 代码时,Kotlin 识别可为 null 注解,并将根据它们的注解处理类型

此外,你可以使用 @Nullable 注解来标记:

  • 可选的控制器方法参数。 可选的注射点。例如,当使用构造函数注入时,可以通过添加 @Nullable 注解将构造函数参数注解为可选参数。

3.20 Micronaut Bean 和 Spring

Micronaut 以多种形式与 Spring 集成。更多信息,参阅 Micronaut Spring 文档

3.21 Android 支持

由于 Micronaut 依赖注入基于注解处理器,不依赖反射,因此在使用 Android 插件 3.0.0 或更高版本时,可以在 Android 上使用它。

这使你可以为 Android 客户端和服务器实现使用相同的应用程序框架。

配置 Android 构建

要开始,请使用 annotationProcessor 依赖配置将 Micronaut 注解处理器添加到处理器 classpath。

在 Android 构建配置的 annotationProcessorcompileOnly 范围中包含 Micronaut micronaut-inject-java

示例:Android build.gradle

dependencies {
...
annotationProcessor "io.micronaut:micronaut-inject-java:3.9.4"
compileOnly "io.micronaut:micronaut-inject-java:3.9.4"
...
}

如果你将 lint 作为构建的一部分,你可能还需要禁用无效包检查,因为 Android 包含一个硬编码检查,该检查将j avax.inject 包视为无效,除非你使用 Dagger:

示例:使用 lint 的 build.gradle

android {
...
lintOptions {
lintOptions { warning 'InvalidPackage' }
}
}

你可以在 Android 文档中找到有关配置注解处理器的更多信息。

注意

Micronaut inject-java 依赖使用 Android java 8 支持功能。

启用依赖注入

一旦正确配置了 classpath,下一步就是启动 ApplicationContext

以下示例演示了为此创建 android.app.Application 的子类:

示例:Android Application 类

import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;

public class BaseApplication extends Application { (1)

private ApplicationContext ctx;

@Override
public void onCreate() {
super.onCreate();
ctx = ApplicationContext.run(MainActivity.class, Environment.ANDROID); (2)
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { (3)
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
ctx.inject(activity);
}
... // shortened for brevity; it is not necessary to implement other methods
});
}
}
  1. 继承 android.app.Application
  2. 使用 ANDROID 环境运行 ApplicationContext
  3. 注册 ActivityLifecycleCallbacks 实例以允许 Android Activity 实例的依赖注入

英文链接