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)
}
- 定义 Micronaut Library 插件
- 指定使用的 Micronaut 版本
IoC 的入口点是 ApplicationContext 接口,它包括一个 run
方法。以下示例演示如何使用它:
运行 ApplicationContext
try (ApplicationContext context = ApplicationContext.run()) { (1)
MyBean myBean = context.getBean(MyBean.class); (2)
// do something with your bean
}
- 运行 ApplicationContext
- 从 ApplicationContext 获取一个 bean
该示例使用 Java try-with-resources 语法来确保 ApplicationContext 在应用程序退出时完全关闭
3.1 定义 Bean
bean 是一个对象,其生命周期由 Micronaut IoC 容器管理。该生命周期可能包括创建、执行和销毁。Micronaut 实现了 JSR-330(javax.inject—— Java 依赖注入 规范,因此要使用 Micronaut,只需使用 javax.inject 提供的注解即可。
以下是一个简单的示例:
- Java
- Groovy
- Kotlin
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();
}
}
interface Engine { // (1)
int getCylinders()
String start()
}
@Singleton // (2)
class V8Engine implements Engine {
int cylinders = 8
@Override
String start() {
"Starting V8"
}
}
@Singleton
class Vehicle {
final Engine engine
Vehicle(Engine engine) { // (3)
this.engine = engine
}
String start() {
engine.start()
}
}
interface Engine {
// (1)
val cylinders: Int
fun start(): String
}
@Singleton// (2)
class V8Engine : Engine {
override var cylinders = 8
override fun start(): String {
return "Starting V8"
}
}
@Singleton
class Vehicle(private val engine: Engine) { // (3)
fun start(): String {
return engine.start()
}
}
- 定义了通用
Engine
接口 V8Engine
实现被定义并标记为Singleton
作用域- 通过构造函数注入来注入
Engine
要执行依赖注入,请使用 run()
方法运行 BeanContext,并使用 getBean(Class)
查找 bean,如下例所示:
- Java
- Groovy
- Kotlin
final BeanContext context = BeanContext.run();
Vehicle vehicle = context.getBean(Vehicle.class);
System.out.println(vehicle.start());
def context = BeanContext.run()
Vehicle vehicle = context.getBean(Vehicle)
println vehicle.start()
val context = BeanContext.run()
val vehicle = context.getBean(Vehicle::class.java)
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.Optional | Optional 的 bean。如果 bean 不存在,注入 empty() | Optional<Engine> |
java.lang.Collection | Collection 或 Collection 子类型(如,List 、Set 等) | Collection<Engine> |
java.util.stream.Stream | 懒 Stream 的 bean | Stream<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.Collection
或 java.util.stream.stream
、Array
的 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 类型是第三方库中的类。在此示例中,LowRateLimit
和 HighRateLimit
都实现 RateLimit
接口。
带 @Order 的工厂
- Java
- Groovy
- Kotlin
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);
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.Order
import jakarta.inject.Singleton
import java.time.Duration
@Factory
class RateLimitsFactory {
@Singleton
@Order(20)
LowRateLimit rateLimit2() {
new LowRateLimit(Duration.ofMinutes(50), 100);
}
@Singleton
@Order(10)
HighRateLimit rateLimit1() {
new HighRateLimit(Duration.ofMinutes(50), 1000);
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.Order
import java.time.Duration
import jakarta.inject.Singleton
@Factory
class RateLimitsFactory {
@Singleton
@Order(20)
fun rateLimit2(): LowRateLimit {
return LowRateLimit(Duration.ofMinutes(50), 100)
}
@Singleton
@Order(10)
fun rateLimit1(): HighRateLimit {
return HighRateLimit(Duration.ofMinutes(50), 1000)
}
}
当从上下文请求 RateLimit
bean 集合时,将根据注解中的值以升序返回它们。
按顺序注入 Bean
当注入 bean 的单个实例时,@Order 注解也可以用于定义哪个 bean 具有最高优先级,因而应该注入。
选择单个实例时不考虑 Ordered 接口,因为这需要实例化 bean 来解析顺序
3.5 Bean 限定符
如果给定接口有多个可能的实现要注入,则需要使用限定符。
Micronaut 再次利用 JSR-330 以及 Qualifier 和 Named 注解来支持此用例。
按名字限定
要按名称限定,请使用 Named 注解。例如,考虑以下类:
- Java
- Groovy
- Kotlin
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)
}
}
interface Engine { // (1)
int getCylinders()
String start()
}
@Singleton
class V6Engine implements Engine { // (2)
int cylinders = 6
@Override
String start() {
"Starting V6"
}
}
@Singleton
class V8Engine implements Engine { // (3)
int cylinders = 8
@Override
String start() {
"Starting V8"
}
}
@Singleton
class Vehicle {
final Engine engine
@Inject Vehicle(@Named('v8') Engine engine) { // (4)
this.engine = engine
}
String start() {
engine.start() // (5)
}
}
interface Engine { // (1)
val cylinders: Int
fun start(): String
}
@Singleton
class V6Engine : Engine { // (2)
override var cylinders: Int = 6
override fun start(): String {
return "Starting V6"
}
}
@Singleton
class V8Engine : Engine {
override var cylinders: Int = 8
override fun start(): String {
return "Starting V8"
}
}
@Singleton
class Vehicle @Inject
constructor(@param:Named("v8") private val engine: Engine) { // (4)
fun start(): String {
return engine.start() // (5)
}
}
Engine
接口定义通用合同V6Engine
类是第一个实现V8Engine
类是第二个实现- javax.inject.Named 注解表示需要
V8Engine
实现 - 调用 start 方法打印:
"Starting V8"
Micronaut 能够在前面的示例中注入 V8Engine
,因为:
@Named
限定符(v8
)+ 注入的类型简单名称(Engine
)==(不区分大小写)== Engine
类型的 bean 的简单名称(V8Engine
)
你还可以在 bean 的类级别声明 @Named,以显式定义 bean 的名称。
按注解限定
除了能够按名称限定外,还可以使用 Qualifier 注解构建自己的限定符。例如,考虑以下注解:
- Java
- Groovy
- Kotlin
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}
import jakarta.inject.Qualifier
import java.lang.annotation.Retention
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Qualifier
@Retention(RUNTIME)
@interface V8 {
}
import jakarta.inject.Qualifier
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy.RUNTIME
@Qualifier
@Retention(RUNTIME)
annotation class V8
上面的注解本身使用 @Qualifier
进行注解,以将其指定为限定符。然后可以在代码中的任何注入点使用注解。例如:
- Java
- Groovy
- Kotlin
@Inject Vehicle(@V8 Engine engine) {
this.engine = engine;
}
@Inject Vehicle(@V8 Engine engine) {
this.engine = engine
}
@Inject constructor(@V8 val engine: Engine) {
按注解成员限定
从 Micronaut 3.0 开始,注解限定符也可以使用注解成员来解决正确的 bean 注入。例如,考虑下面这个注解:
- Java
- Groovy
- Kotlin
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 "";
}
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)
@interface Cylinders {
int value();
@NonBinding // (2)
String description() default "";
}
import io.micronaut.context.annotation.NonBinding
import jakarta.inject.Qualifier
import kotlin.annotation.Retention
@Qualifier // (1)
@Retention(AnnotationRetention.RUNTIME)
annotation class Cylinders(
val value: Int,
@get:NonBinding // (2)
val description: String = ""
)
@Cylinders
注解使用@Qualifier
进行元注解- 注解有两个成员。@NonBinding 注解用于在依赖项解析期间排除描述成员。
然后,你可以在任何 bean 上使用 @Cylinders
注解,并且在依赖关系解析期间会考虑未使用 @NonBinding 注解的成员:
- Java
- Groovy
- Kotlin
@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";
}
}
@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine") // (1)
class V6Engine implements Engine { // (2)
@Override
int getCylinders() {
return 6
}
@Override
String start() {
return "Starting V6"
}
}
@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine") // (1)
class V6Engine : Engine { // (2)
// (2)
override val cylinders: Int
get() = 6
override fun start(): String {
return "Starting V6"
}
}
- 此处,
V6Engine
类型的value
成员设置为6
- 该类实现
Engine
接口
- Java
- Groovy
- Kotlin
@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";
}
}
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
class V8Engine implements Engine { // (2)
@Override
int getCylinders() {
return 8
}
@Override
String start() {
return "Starting V8"
}
}
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
class V8Engine : Engine { // (2)
override val cylinders: Int
get() = 8
override fun start(): String {
return "Starting V8"
}
}
- 这里,
V8Engine
类型的value
成员设置为8
- 该类实现
Engine
接口
然后可以在任何注入点上使用 @Cylinder
限定符来选择要注入的正确 bean。例如:
- Java
- Groovy
- Kotlin
@Inject Vehicle(@Cylinders(8) Engine engine) {
this.engine = engine;
}
interface CylinderProvider {
int getCylinders()
}
@Singleton
class Vehicle(@param:Cylinders(8) val engine: Engine) {
fun start(): String {
return engine.start()
}
}
按泛型类型参数限定
从 Micronaut 3.0 开始,可以根据类或接口的泛型类型参数选择要注入的 bean。考虑以下示例:
- Java
- Groovy
- Kotlin
public interface CylinderProvider {
int getCylinders();
}
interface CylinderProvider {
int getCylinders()
}
interface CylinderProvider {
val cylinders: Int
}
CylinderProvider
接口提供 cylinder 数。
- Java
- Groovy
- Kotlin
public interface Engine<T extends CylinderProvider> { // (1)
default int getCylinders() {
return getCylinderProvider().getCylinders();
}
default String start() {
return "Starting " + getCylinderProvider().getClass().getSimpleName();
}
T getCylinderProvider();
}
interface Engine<T extends CylinderProvider> { // (1)
default int getCylinders() { cylinderProvider.cylinders }
default String start() { "Starting ${cylinderProvider.class.simpleName}" }
T getCylinderProvider()
}
interface Engine<T : CylinderProvider> { // (1)
val cylinders: Int
get() = cylinderProvider.cylinders
fun start(): String {
return "Starting ${cylinderProvider.javaClass.simpleName}"
}
val cylinderProvider: T
}
- 引擎类定义了一个泛型类型参数
<T>
,该参数必须是CylinderProvider
的实例
你可以使用不同的泛型类型参数定义 Engine
接口的实现。例如,对于 V6 engine:
- Java
- Groovy
- Kotlin
public class V6 implements CylinderProvider {
@Override
public int getCylinders() {
return 6;
}
}
@Singleton
class V6Engine implements Engine<V6> { // (1)
@Override
V6 getCylinderProvider() { new V6() }
}
class V8 : CylinderProvider {
override val cylinders: Int = 8
}
上面定义了一个实现 CylinderProvider
接口的 V8 类:
- Java
- Groovy
- Kotlin
@Inject
public Vehicle(Engine<V8> engine) {
this.engine = engine;
}
@Inject
Vehicle(Engine<V8> engine) {
this.engine = engine
}
@Singleton
class V8Engine : Engine<V8> { // (1)
override val cylinderProvider: V8
get() = V8()
}
首选及备选 Bean
Primary 是一个限定符,表示在多个接口实现的情况下,bean 要选择的首选 bean。
考虑以下示例:
- Java
- Groovy
- Kotlin
public interface ColorPicker {
String color();
}
interface ColorPicker {
String color()
}
interface ColorPicker {
fun color(): String
}
ColorPicker
由以下类实现:
首选 Bean
- Java
- Groovy
- Kotlin
import io.micronaut.context.annotation.Primary;
import jakarta.inject.Singleton;
@Primary
@Singleton
class Green implements ColorPicker {
@Override
public String color() {
return "green";
}
}
import io.micronaut.context.annotation.Primary
import jakarta.inject.Singleton
@Primary
@Singleton
class Green implements ColorPicker {
@Override
String color() {
return "green"
}
}
import io.micronaut.context.annotation.Primary
import jakarta.inject.Singleton
@Primary
@Singleton
class Green: ColorPicker {
override fun color(): String {
return "green"
}
}
Green
bean 类实现 ColorPicker
,并用 @Primary
注解。
同类型的另一个 Bean
- Java
- Groovy
- Kotlin
import jakarta.inject.Singleton;
@Singleton
public class Blue implements ColorPicker {
@Override
public String color() {
return "blue";
}
}
import jakarta.inject.Singleton
@Singleton
class Blue implements ColorPicker {
@Override
String color() {
return "blue"
}
}
import jakarta.inject.Singleton
@Singleton
class Blue: ColorPicker {
override fun color(): String {
return "blue"
}
}
Blue
bean 类还实现了 ColorPicker
,因此在注入 ColorPicker
接口时有两个可能的候选对象。由于 Green
是首选的,因此它将一直受到青睐。
- Java
- Groovy
- Kotlin
@Controller("/testPrimary")
public class TestController {
protected final ColorPicker colorPicker;
public TestController(ColorPicker colorPicker) { // (1)
this.colorPicker = colorPicker;
}
@Get
public String index() {
return colorPicker.color();
}
}
@Controller("/test")
class TestController {
protected final ColorPicker colorPicker
TestController(ColorPicker colorPicker) { // (1)
this.colorPicker = colorPicker
}
@Get
String index() {
colorPicker.color()
}
}
@Controller("/test")
class TestController(val colorPicker: ColorPicker) { // (1)
@Get
fun index(): String {
return colorPicker.color()
}
}
- 虽然有两个
ColorPicker
bean,但由于@Primary
注解,Green
被注入
如果存在多个可能的候选项,并且未定义 @Primary
,则引发 NonUniqueBeaException。
除了 @Primary
,还有一个 Secondary 注解,它会产生相反的效果,并允许取消 bean 的优先级。
注入任意 Bean
如果你不知道注入哪个 bean,那么可以使用 @Any 限定符来注入第一个可用的 bean,例如:
注入任意 Bean
- Java
- Groovy
- Kotlin
@Inject @Any
Engine engine;
@Inject @Any
Engine engine
@Inject
@field:Any
lateinit var engine: Engine
@Any 限定符通常与 BeanProvider 接口一起使用,以允许更动态的用例。例如,如果 bean 存在,以下 Vehicle
实现将启动 Engine
:
带 Any 使用 BeanProvider
- Java
- Groovy
- Kotlin
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)
}
}
import io.micronaut.context.BeanProvider
import io.micronaut.context.annotation.Any
import jakarta.inject.Singleton
@Singleton
class Vehicle {
final BeanProvider<Engine> engineProvider
Vehicle(@Any BeanProvider<Engine> engineProvider) { // (1)
this.engineProvider = engineProvider
}
void start() {
engineProvider.ifPresent(Engine::start) // (2)
}
}
import io.micronaut.context.BeanProvider
import io.micronaut.context.annotation.Any
import jakarta.inject.Singleton
@Singleton
class Vehicle(@param:Any val engineProvider: BeanProvider<Engine>) { // (1)
fun start() {
engineProvider.ifPresent { it.start() } // (2)
}
fun startAll() {
if (engineProvider.isPresent) { // (1)
engineProvider.forEach { it.start() } // (2)
}
}
- 使用
@Any
注入 BeanProvider - 如果使用
ifPresent
方法存在基础 bean,则调用start
方法
如果有多个 bean,你也可以调整行为。以下示例启动 Vehicle
中安装的所有发动机(如果有):
带 Any 使用 BeanProvider
- Java
- Groovy
- Kotlin
void startAll() {
if (engineProvider.isPresent()) { // (1)
engineProvider.stream().forEach(Engine::start); // (2)
}
}
void startAll() {
if (engineProvider.isPresent()) { // (1)
engineProvider.each {it.start() } // (2)
}
}
fun startAll() {
if (engineProvider.isPresent) { // (1)
engineProvider.forEach { it.start() } // (2)
}
- 检查是否有 bean
- 如果是这样,则通过
stream().forEach(..)
方法迭代每个引擎,启动引擎
3.6 限制可注入类型
默认情况下,当你使用作用域(如 @Singleton
)注解 bean 时,bean 类及其实现的所有接口和扩展的超级类都可以通过 @Inject
注入。
考虑上一节中关于定义 bean 的以下示例:
- Java
- Groovy
- Kotlin
@Singleton
public class V8Engine implements Engine { // (3)
@Override
public String start() {
return "Starting V8";
}
@Override
public int getCylinders() {
return 8;
}
}
@Singleton
class V8Engine implements Engine { // (3)
int cylinders = 8
@Override
String start() {
"Starting V8"
}
}
@Singleton
class V8Engine : Engine {
override var cylinders: Int = 8
override fun start(): String {
return "Starting V8"
}
}
在上述情况下,应用程序中的其他类可以选择注入接口 Engine
或具体实现 V8Engine
。
如果这是不希望的,可以使用 @Bean 注解的 typed
成员来限制公开的类型。例如:
- Java
- Groovy
- Kotlin
@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;
}
}
@Singleton
@Bean(typed = Engine) // (1)
class V8Engine implements Engine { // (2)
@Override
String start() { "Starting V8" }
@Override
int getCylinders() { 8 }
}
@Singleton
@Bean(typed = [Engine::class]) // (1)
class V8Engine : Engine { // (2)
override fun start(): String {
return "Starting V8"
}
override val cylinders: Int = 8
}
@Bean(typed=..)
用于仅允许注入接口Engine
,而不允许注入具体类型- 该类必须实现由
typed
定义的类或接口,否则将发生编译错误
以下测试演示了使用编程查找和 BeanContext API 进行 typed
的行为:
- Java
- Groovy
- Kotlin
@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);
}
}
class EngineSpec extends Specification {
@Shared @AutoCleanup
ApplicationContext beanContext = ApplicationContext.run()
void 'test engine'() {
when:'the class is looked up'
beanContext.getBean(V8Engine) // (1)
then:'a no such bean exception is thrown'
thrown(NoSuchBeanException)
and:'it is possible to lookup by the typed interface'
beanContext.getBean(Engine) instanceof V8Engine // (2)
}
}
@MicronautTest
class EngineSpec {
@Inject
lateinit var beanContext: BeanContext
@Test
fun testEngine() {
assertThrows(NoSuchBeanException::class.java) {
beanContext.getBean(V8Engine::class.java) // (1)
}
val engine = beanContext.getBean(Engine::class.java) // (2)
assertTrue(engine is V8Engine)
}
}
- 尝试查找
V8Engine
引发 NoSuchBeaException - 查找
Engine
接口时成功
3.7 作用域
Micronaut 具有基于 JSR-330 的可扩展 bean 作用域机制。支持以下默认作用域:
3.7.1 内置作用域
表 1.Micronaut 内置作用域
类型 | 描述 |
---|---|
@Singleton | Singleton 作用域表示只存在bean的一个实例 |
@Context | Context 作用域表示 bean 将与 ApplicationContext 同时创建(急切初始化) |
@Prototype | Prototype 作用域表示每次注入 bean 时都会创建一个新的 bean 实例 |
@Infrastructure | Infrastructure 作用域表示不能使用 @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();
}
}
- 将急切初始化设置为
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();
}
}
- 将急切初始化设置为
true
将初始化所有配置读取器 bean
3.7.2 Refreshable 作用域
Refreshable 作用域是一个自定义作用域,允许通过以下方式刷新 bean 的状态:
/refresh
端点。- RefreshEvent 的发布。
以下示例说明了 @Refreshable
作用域的行为。
- Java
- Groovy
- Kotlin
@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;
}
}
@Refreshable // (1)
static class WeatherService {
String forecast
@PostConstruct
void init() {
forecast = "Scattered Clouds ${new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(new Date())}" // (2)
}
String latestForecast() {
return forecast
}
}
@Refreshable // (1)
open class WeatherService {
private var forecast: String? = null
@PostConstruct
open fun init() {
forecast = "Scattered Clouds " + SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(Date())// (2)
}
open fun latestForecast(): String? {
return forecast
}
}
WeatherService
使用@Refreshable
作用域进行注解,它存储实例,直到触发刷新事件- 在创建 bean 时,
forecast
属性的值设置为固定值,在刷新 bean 之前不会更改
如果你两次调用 latestForecast()
,你将看到相同的响应,如 "Scattered Clouds 01/Feb/18 10:29.199"
。
当调用 /refresh
端点或发布 RefreshEvent 时,该实例将无效,并在下次请求对象时创建新实例。例如:
- Java
- Groovy
- Kotlin
applicationContext.publishEvent(new RefreshEvent());
applicationContext.publishEvent(new RefreshEvent())
applicationContext.publishEvent(RefreshEvent())
3.7.3 元注解作用域
可以在元注解上定义作用域,然后可以将其应用于类。考虑以下元注解示例:
Driver java 注解
- Java
- Groovy
- Kotlin
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 {
}
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)
@interface Driver {
}
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton
import kotlin.annotation.AnnotationRetention.RUNTIME
@Requires(classes = [Car::class]) // (1)
@Singleton // (2)
@MustBeDocumented
@Retention(RUNTIME)
annotation class Driver
- 作用域使用 Requires 声明
Car
类上的需求 - 注解声明为
@Singleton
在上面的示例中,@Singleton
注解应用于 @Driver
注解,这会导致用 @Driver
进行注解的每个类都被视为单例。
注意,在这种情况下,应用注解时不可能更改作用域。例如,以下内容不会覆盖 @Driver
声明的作用域,并且无效:
声明另一个作用域
@Driver
@Prototype
class Foo {}
要使作用域可重写,请在 @Driver
上使用 DefaultScope 注解,如果没有其他作用域,则允许指定默认作用域:
使用 @DefaultScope
- Java
- Groovy
- Kotlin
@Requires(classes = Car.class)
@DefaultScope(Singleton.class) (1)
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
@Requires(classes = Car.class)
@DefaultScope(Singleton.class) (1)
@Documented
@Retention(RUNTIME)
@interface Driver {
}
@Requires(classes = [Car::class])
@DefaultScope(Singleton::class) (1)
@Documented
@Retention(RUNTIME)
annotation class Driver
- DefaultScope 声明了未指定时要使用的作用域
3.8 Bean 工厂
在许多情况下,你可能希望将不属于代码库的类(如第三方库提供的类)作为bean提供。在这种情况下,不能对编译的类进行注解。相反,实现一个 @Factory。
工厂是一个用 Factory 注解的注解类,它提供了一个或多个注解的方法(用 bean 作用域注解)。你使用的注解取决于你希望 bean 位于哪个作用域中。更多信息,参阅 bean 作用域一节。
工厂具有默认作用域 singleton ,并将随上下文一起销毁。如果你想在工厂生成 bean 后处理它,请使用 @Prototype
作用域。
用 bean 作用域注解来注解的方法的返回类型是 bean 类型。这最好用一个例子来说明:
- Java
- Groovy
- Kotlin
@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);
}
}
@Singleton
class CrankShaft {
}
class V8Engine implements Engine {
final int cylinders = 8
final CrankShaft crankShaft
V8Engine(CrankShaft crankShaft) {
this.crankShaft = crankShaft
}
@Override
String start() {
"Starting V8"
}
}
@Factory
class EngineFactory {
@Singleton
Engine v8Engine(CrankShaft crankShaft) {
new V8Engine(crankShaft)
}
}
@Singleton
internal class CrankShaft
internal class V8Engine(private val crankShaft: CrankShaft) : Engine {
private val cylinders = 8
override fun start(): String {
return "Starting V8"
}
}
@Factory
internal class EngineFactory {
@Singleton
fun v8Engine(crankShaft: CrankShaft): Engine {
return V8Engine(crankShaft)
}
}
在本例中,V8Engine
由 EngineFactory
类的 V8Engine
方法创建。注意,你可以将参数注入到方法中,它们将被解析为 bean。生成的 V8Engine
bean 将是一个单例。
一个工厂可以有多个用 bean 作用域注解的方法,每个方法都返回一个不同的 bean 类型。
如果采用这种方法,则不应在类内部调用其他 bean 方法。相反,通过参数注入类型。
要允许生成的 bean 参与应用程序上下文关闭过程,请使用 @Bean 注解该方法,并将 preDestroy
参数设置为要调用以关闭 bean 的方法的名称。
来自字段的 Bean
使用 Micronaut 3.0 或更高版本,也可以通过在字段上声明 @Bean 注解来从字段生成 Bean。
虽然一般情况下,这种方法应该不鼓励使用工厂方法,因为工厂方法提供了更多的灵活性,但它确实简化了测试代码。例如,使用 bean 字段,你可以在测试代码中轻松生成模拟:
- Java
- Groovy
- Kotlin
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)
}
}
import io.micronaut.context.annotation.*
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import jakarta.inject.Inject
@MicronautTest
class VehicleMockSpec extends Specification {
@Requires(beans=VehicleMockSpec.class)
@Bean @Replaces(Engine.class)
Engine mockEngine = {-> "Mock Started" } as Engine // (1)
@Inject Vehicle vehicle // (2)
void "test start engine"() {
given:
final String result = vehicle.start()
expect:
result == "Mock Started" // (3)
}
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Replaces
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import jakarta.inject.Inject
@MicronautTest
class VehicleMockSpec {
@get:Bean
@get:Replaces(Engine::class)
val mockEngine: Engine = object : Engine { // (1)
override fun start(): String {
return "Mock Started"
}
}
@Inject
lateinit var vehicle : Vehicle // (2)
@Test
fun testStartEngine() {
val result = vehicle.start()
Assertions.assertEquals("Mock Started", result) // (3)
}
}
- bean 是从替换现有
Engine
的字段中定义的。 Vehicle
被注入。- 代码断言调用了模拟实现。
请注意,非基元类型仅支持公共或包保护字段。如果字段是 static
、private
或 protected
的,则会发生编译错误。
如果bean方法/字段包含作用域或限定符,则将省略该类型中的任何作用域或限制符。
工厂实例的限定符不会继承到bean
基本 bean 和数组
从 Micronaut 3.1 开始,可以从工厂定义和注入基本类型和数组类型。
例如:
- Java
- Groovy
- Kotlin
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;
}
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
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Named
@Factory
class CylinderFactory {
@get:Bean
@get:Named("V8") // (1)
val v8 = 8
@get:Bean
@get:Named("V6") // (1)
val v6 = 6
}
- 使用不同的名称定义了两个基本整数 bean
基本 bean 可以像任何其他 bean 一样被注入:
- Java
- Groovy
- Kotlin
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;
}
}
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class V8Engine {
private final int cylinders
V8Engine(@Named("V8") int cylinders) { // (1)
this.cylinders = cylinders
}
int getCylinders() {
return cylinders
}
}
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class V8Engine(
@param:Named("V8") val cylinders: Int // (1)
)
请注意,基元 bean 和基本数组 bean 具有以下限制:
- AOP advice 不能应用于原语或包装器类型
- 由于上述自定义作用域,不支持代理
- 不支持
@Bean(preDestroy=..)
成员
编程禁用 Bean
工厂方法可以抛出 DisabledBeanException 以有条件地禁用 bean。使用 @Requires 应该始终是有条件地创建 bean 的首选方法;只有在无法使用 @Requires 时,才能在工厂方法中引发异常。
例如:
- Java
- Groovy
- Kotlin
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");
}
}
}
interface Engine {
Integer getCylinders()
}
@EachProperty("engines")
class EngineConfiguration implements Toggleable {
boolean enabled = true
@NotNull
Integer cylinders
}
@Factory
class EngineFactory {
@EachBean(EngineConfiguration)
Engine buildEngine(EngineConfiguration engineConfiguration) {
if (engineConfiguration.enabled) {
(Engine){ -> engineConfiguration.cylinders }
} else {
throw new DisabledBeanException("Engine configuration disabled")
}
}
}
interface Engine {
fun getCylinders(): Int
}
@EachProperty("engines")
class EngineConfiguration : Toggleable {
var enabled = true
@NotNull
val cylinders: Int? = null
override fun isEnabled(): Boolean {
return enabled
}
}
@Factory
class EngineFactory {
@EachBean(EngineConfiguration::class)
fun buildEngine(engineConfiguration: EngineConfiguration): Engine? {
return if (engineConfiguration.isEnabled) {
object : Engine {
override fun getCylinders(): Int {
return engineConfiguration.cylinders!!
}
}
} else {
throw DisabledBeanException("Engine configuration disabled")
}
}
}
注入点
工厂的一个常见用例是从注入对象的点利用注解元数据,从而可以基于所述元数据修改行为。
考虑以下注解:
- Java
- Groovy
- Kotlin
@Documented
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Cylinders {
int value() default 8;
}
@Documented
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
@interface Cylinders {
int value() default 8
}
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Cylinders(val value: Int = 8)
上述注解可用于自定义我们希望在定义的注入点处注入到车辆中的发动机类型:
- Java
- Groovy
- Kotlin
@Singleton
class Vehicle {
private final Engine engine;
Vehicle(@Cylinders(6) Engine engine) {
this.engine = engine;
}
String start() {
return engine.start();
}
}
@Singleton
class Vehicle {
private final Engine engine
Vehicle(@Cylinders(6) Engine engine) {
this.engine = engine
}
String start() {
return engine.start()
}
}
@Singleton
internal class Vehicle(@param:Cylinders(6) private val engine: Engine) {
fun start(): String {
return engine.start()
}
}
上述 Vehicle
类指定了 @Cylinders(6)
的注解值,表示需要六个气缸的 Engine
。
要实现此用例,请定义一个接受 InjectionPoint 实例的工厂,以分析定义的注解值:
- Java
- Groovy
- Kotlin
@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);
}
}
}
@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")
}
}
}
@Factory
internal class EngineFactory {
@Prototype
fun v8Engine(injectionPoint: InjectionPoint<*>, crankShaft: CrankShaft): Engine { // (1)
val cylinders = injectionPoint
.annotationMetadata
.intValue(Cylinders::class.java).orElse(8) // (2)
return when (cylinders) { // (3)
6 -> V6Engine(crankShaft)
8 -> V8Engine(crankShaft)
else -> throw IllegalArgumentException("Unsupported number of cylinders specified: $cylinders")
}
}
}
- 工厂方法定义了 InjectionPoint 类型的参数。
- 注解元数据用于获取
@Cylinder
注解的值 - 该值用于构造引擎,如果无法构造引擎,则引发异常。
需要注意的是,工厂声明为 @Prototype 作用域,因此每个注入点都会调用该方法。如果 V8Engine
和 V6Engine
类型需要是单体的,工厂应该使用 Map 来确保对象只构造一次
3.9 条件 Bean
有时,你可能希望基于各种潜在因素,包括 classpath、配置、其他bean的存在等,有条件地加载bean。
Requires 注解提供了在 bean 上定义一个或多个条件的能力。
考虑以下示例:
使用 @Requires
- Java
- Groovy
- Kotlin
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {
DataSource dataSource;
public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {
DataSource dataSource
@Singleton
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
class JdbcBookService(internal var dataSource: DataSource) : BookService {
上面的 bean 定义了两个需求。第一个指示必须存在 DataSource
bean 才能加载该 bean。第二个要求确保在加载 JdbcBookService
bean 之前设置 datasource.url
属性。
Kotlin 目前不支持可重复注解。当需要多个需求时,使用 @Requirements
注解。例如,@Requirements(Requires(…), Requires(…))
。参阅 https://youtrack.jetbrains.com/issue/KT-12794 以跟踪此功能。
如果多个 bean 需要相同的需求组合,则可以使用要求定义元注解:
使用 @Requires 元注解
- Java
- Groovy
- Kotlin
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public @interface RequiresJdbc {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PACKAGE, ElementType.TYPE])
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
@interface RequiresJdbc {
}
@Documented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
annotation class 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)
- 需要设置属性
- 要求属性为 "John"
- 要求属性为 "John" 或未设置
- 要求属性不为 "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)
- 要求
Config
bean 上的 "foo" 属性 - 要求
Config
bean 上的 "foo" 属性为 "John" - 要求
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
- Java
- Groovy
- Kotlin
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {
DataSource dataSource;
public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {
DataSource dataSource
@Singleton
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
class JdbcBookService(internal var dataSource: DataSource) : BookService {
你可以在 src/test/java
中定义一个类,该类仅用于测试:
使用 @Replaces
- Java
- Groovy
- Kotlin
@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);
}
}
@Replaces(JdbcBookService.class) // (1)
@Singleton
class MockBookService implements BookService {
Map<String, Book> bookMap = [:]
@Override
Book findBook(String title) {
bookMap.get(title)
}
}
@Replaces(JdbcBookService::class) // (1)
@Singleton
class MockBookService : BookService {
var bookMap: Map<String, Book> = LinkedHashMap()
override fun findBook(title: String): Book? {
return bookMap[title]
}
}
MockBookService
声明它将替换JdbcBookService
工厂替换
@Replaces
注解还支持 factory
参数。该参数允许替换整个工厂 bean 或工厂创建的特定类型。
例如,可能需要替换所有或部分给定工厂类别:
BookFactory
- Java
- Groovy
- Kotlin
@Factory
public class BookFactory {
@Singleton
Book novel() {
return new Book("A Great Novel");
}
@Singleton
TextBook textBook() {
return new TextBook("Learning 101");
}
}
@Factory
class BookFactory {
@Singleton
Book novel() {
new Book('A Great Novel')
}
@Singleton
TextBook textBook() {
new TextBook('Learning 101')
}
}
@Factory
class BookFactory {
@Singleton
internal fun novel(): Book {
return Book("A Great Novel")
}
@Singleton
internal fun textBook(): TextBook {
return TextBook("Learning 101")
}
}
要完全替换工厂,工厂方法必须与替换工厂中所有方法的返回类型匹配。
在本例中,BookFactory#textBook()
未被替换,因为该工厂没有返回 TextBook
的工厂方法。
CustomBookFactory
- Java
- Groovy
- Kotlin
@Factory
@Replaces(factory = BookFactory.class)
public class CustomBookFactory {
@Singleton
Book otherNovel() {
return new Book("An OK Novel");
}
}
@Factory
@Replaces(factory = BookFactory)
class CustomBookFactory {
@Singleton
Book otherNovel() {
new Book('An OK Novel')
}
}
@Factory
@Replaces(factory = BookFactory::class)
class CustomBookFactory {
@Singleton
internal fun otherNovel(): Book {
return Book("An OK Novel")
}
}
要替换一个或多个工厂方法,但保留其余方法,请在方法上应用 @Replaces
注解,并表示要应用的工厂。
TextBookFactory
- Java
- Groovy
- Kotlin
@Factory
public class TextBookFactory {
@Singleton
@Replaces(value = TextBook.class, factory = BookFactory.class)
TextBook textBook() {
return new TextBook("Learning 305");
}
}
@Factory
class TextBookFactory {
@Singleton
@Replaces(value = TextBook, factory = BookFactory)
TextBook textBook() {
new TextBook('Learning 305')
}
}
@Factory
class TextBookFactory {
@Singleton
@Replaces(value = TextBook::class, factory = BookFactory::class)
internal fun textBook(): TextBook {
return TextBook("Learning 305")
}
}
BookFactory#novel()
方法不会被替换,因为 TextBook 类是在注解中定义的。
默认实现
在公开 API 时,最好不要将接口的默认实现公开为公共 API 的一部分。这样做会阻止用户替换实现,因为他们将无法引用类。解决方案是用 DefaultImplementation 注解接口,以指示如果用户创建了 @Replaces(YourInterface.class
的bean,则要替换哪个实现。
例如,考虑:
public API 约定
- Java
- Groovy
- Kotlin
import io.micronaut.context.annotation.DefaultImplementation;
@DefaultImplementation(DefaultResponseStrategy.class)
public interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation
@DefaultImplementation(DefaultResponseStrategy)
interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation
@DefaultImplementation(DefaultResponseStrategy::class)
interface ResponseStrategy
默认实现
- Java
- Groovy
- Kotlin
import jakarta.inject.Singleton;
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import jakarta.inject.Singleton
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}
import jakarta.inject.Singleton
@Singleton
internal class DefaultResponseStrategy : ResponseStrategy
自定义实现
- Java
- Groovy
- Kotlin
import io.micronaut.context.annotation.Replaces;
import jakarta.inject.Singleton;
@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton
@Singleton
@Replaces(ResponseStrategy)
class CustomResponseStrategy implements ResponseStrategy {
}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton
@Singleton
@Replaces(ResponseStrategy::class)
class CustomResponseStrategy : 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
注解:
- Java
- Groovy
- Kotlin
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;
}
}
import jakarta.annotation.PostConstruct // (1)
import jakarta.inject.Singleton
@Singleton
class V8Engine implements Engine {
int cylinders = 8
boolean initialized = false // (2)
@Override
String start() {
if (!initialized) {
throw new IllegalStateException("Engine not initialized!")
}
return "Starting V8"
}
@PostConstruct // (3)
void initialize() {
initialized = true
}
}
import jakarta.annotation.PostConstruct
import jakarta.inject.Singleton
@Singleton
class V8Engine : Engine {
override val cylinders = 8
var initialized = false
private set // (2)
override fun start(): String {
check(initialized) { "Engine not initialized!" }
return "Starting V8"
}
@PostConstruct // (3)
fun initialize() {
initialized = true
}
}
PostConstruct
注解已导入- 定义了需要初始化的字段
- 一个方法用
@PostConstruct
注解,一旦对象被构造并完全注入,就会被调用。
要管理何时构建 bean,请参阅 bean 作用域一节。
当销毁 Bean 时
要在销毁 bean 时调用方法,请使用 jakarta.annotation.PreDestroy
注解:
- Java
- Groovy
- Kotlin
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);
}
}
import jakarta.annotation.PreDestroy // (1)
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicBoolean
@Singleton
class PreDestroyBean implements AutoCloseable {
AtomicBoolean stopped = new AtomicBoolean(false)
@PreDestroy // (2)
@Override
void close() throws Exception {
stopped.compareAndSet(false, true)
}
}
import jakarta.annotation.PreDestroy // (1)
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicBoolean
@Singleton
class PreDestroyBean : AutoCloseable {
internal var stopped = AtomicBoolean(false)
@PreDestroy // (2)
@Throws(Exception::class)
override fun close() {
stopped.compareAndSet(false, true)
}
}
- 导入
PreDestroy
注解 - 方法用
@PreDestroy
注解,并将在上下文关闭时调用。
对于工厂 Bean,Bean 注解中的 preDestroy
值告诉 Micronaut 要调用哪个方法。
- Java
- Groovy
- Kotlin
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 io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton
@Factory
class ConnectionFactory {
@Bean(preDestroy = "stop") // (1)
@Singleton
Connection connection() {
new Connection()
}
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton
@Factory
class ConnectionFactory {
@Bean(preDestroy = "stop") // (1)
@Singleton
fun connection(): Connection {
return Connection()
}
}
- Java
- Groovy
- Kotlin
import java.util.concurrent.atomic.AtomicBoolean;
public class Connection {
AtomicBoolean stopped = new AtomicBoolean(false);
public void stop() { // (2)
stopped.compareAndSet(false, true);
}
}
import java.util.concurrent.atomic.AtomicBoolean
class Connection {
AtomicBoolean stopped = new AtomicBoolean(false)
void stop() { // (2)
stopped.compareAndSet(false, true)
}
}
import java.util.concurrent.atomic.AtomicBoolean
class Connection {
internal var stopped = AtomicBoolean(false)
fun stop() { // (2)
stopped.compareAndSet(false, true)
}
}
preDestroy
值设置在注解上- 注解值与方法名称匹配
简单地实现 Closeable
或 AutoCloseable
接口不足以使 bean 与上下文一起关闭。必须使用上述方法之一。
依赖 Bean
依赖 bean 是构建 bean 时使用的 bean。如果依赖 bean 的作用域为 @Prototype
或未知,它将与实例一起销毁。
3.13 上下文事件
Micronaut 通过上下文支持通用事件系统。ApplicationEventPublisher API 发布事件,ApplicationEventListener API 用于侦听事件。事件系统不限于 Micronaut 发布并支持用户创建的自定义事件。
发布事件
ApplicationEventPublisher API 支持任何类型的事件,尽管 Micronaut 发布的所有事件都继承 ApplicationEvent。
要发布事件,请使用依赖注入获取 ApplicationEventPublisher 的实例,其中泛型类型是事件的类型,并使用事件对象调用 publishEvent
方法。
发布事件
- Java
- Groovy
- Kotlin
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());
}
}
class SampleEvent {
String message = "Something happened"
}
import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Inject
import jakarta.inject.Singleton
@Singleton
class SampleEventEmitterBean {
@Inject
ApplicationEventPublisher<SampleEvent> eventPublisher
void publishSampleEvent() {
eventPublisher.publishEvent(new SampleEvent())
}
}
data class SampleEvent(val message: String = "Something happened")
import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Inject
import jakarta.inject.Singleton
@Singleton
class SampleEventEmitterBean {
@Inject
internal var eventPublisher: ApplicationEventPublisher<SampleEvent>? = null
fun publishSampleEvent() {
eventPublisher!!.publishEvent(SampleEvent())
}
}
默认情况下,发布事件是同步的!在执行所有侦听器之前,publishEvent
方法不会返回。如果时间密集,将此工作移到线程池。
监听事件
要侦听事件,请注册一个实现 ApplicationEventListener 的 bean,其中泛型类型是事件类型。
使用 ApplicationEventListener 侦听事件
- Java
- Groovy
- Kotlin
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());
}
}
}
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.docs.context.events.SampleEvent
import jakarta.inject.Singleton
@Singleton
class SampleEventListener implements ApplicationEventListener<SampleEvent> {
int invocationCounter = 0
@Override
void onApplicationEvent(SampleEvent event) {
invocationCounter++
}
}
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import spock.lang.Specification
class SampleEventListenerSpec extends Specification {
void "test event listener is notified"() {
given:
ApplicationContext context = ApplicationContext.run()
SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean)
SampleEventListener listener = context.getBean(SampleEventListener)
expect:
listener.invocationCounter == 0
when:
emitter.publishSampleEvent()
then:
listener.invocationCounter == 1
cleanup:
context.close()
}
}
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.docs.context.events.SampleEvent
import jakarta.inject.Singleton
@Singleton
class SampleEventListener : ApplicationEventListener<SampleEvent> {
var invocationCounter = 0
override fun onApplicationEvent(event: SampleEvent) {
invocationCounter++
}
}
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.AnnotationSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
class SampleEventListenerSpec : AnnotationSpec() {
@Test
fun testEventListenerWasNotified() {
val context = ApplicationContext.run()
val emitter = context.getBean(SampleEventEmitterBean::class.java)
val listener = context.getBean(SampleEventListener::class.java)
listener.invocationCounter.shouldBe(0)
emitter.publishSampleEvent()
listener.invocationCounter.shouldBe(1)
context.close()
}
}
可以重写 supports 方法以进一步澄清要处理的事件。
或者,如果你不希望实现接口或使用内置事件之一,如 StartupEvent 和 ShutdownEvent,请使用 @EventListener 注解:
使用 @EventListener
监听事件
- Java
- Groovy
- Kotlin
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;
}
}
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
class SampleEventListener {
int invocationCounter = 0
@EventListener
void onSampleEvent(SampleEvent event) {
invocationCounter++
}
@EventListener
void onStartupEvent(StartupEvent event) {
// startup logic here
}
@EventListener
void onShutdownEvent(ShutdownEvent event) {
// shutdown logic here
}
}
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
class SampleEventListener {
var invocationCounter = 0
@EventListener
internal fun onSampleEvent(event: SampleEvent) {
invocationCounter++
}
@EventListener
internal fun onStartupEvent(event: StartupEvent) {
// startup logic here
}
@EventListener
internal fun onShutdownEvent(event: ShutdownEvent) {
// shutdown logic here
}
}
如果侦听器执行的工作可能需要一段时间,请使用 @Async 注解在单独的线程上运行该操作:
使用 @EventListener 异步监听事件
- Java
- Groovy
- Kotlin
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.runtime.event.annotation.EventListener
import io.micronaut.scheduling.annotation.Async
@Singleton
class SampleEventListener {
AtomicInteger invocationCounter = new AtomicInteger(0)
@EventListener
@Async
void onSampleEvent(SampleEvent event) {
invocationCounter.getAndIncrement()
}
}
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
class SampleEventListenerSpec extends Specification {
void "test event listener is notified"() {
given:
def context = ApplicationContext.run()
def emitter = context.getBean(SampleEventEmitterBean)
def listener = context.getBean(SampleEventListener)
expect:
listener.invocationCounter.get() == 0
when:
emitter.publishSampleEvent()
then:
new PollingConditions(timeout: 5).eventually {
listener.invocationCounter.get() == 1
}
cleanup:
context.close()
}
}
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.runtime.event.annotation.EventListener
import io.micronaut.scheduling.annotation.Async
import java.util.concurrent.atomic.AtomicInteger
@Singleton
open class SampleEventListener {
var invocationCounter = AtomicInteger(0)
@EventListener
@Async
open fun onSampleEvent(event: SampleEvent) {
println("Incrementing invocation counter...")
invocationCounter.getAndIncrement()
}
}
import io.kotest.assertions.timing.eventually
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.AnnotationSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import org.opentest4j.AssertionFailedError
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.toDuration
@ExperimentalTime
class SampleEventListenerSpec : AnnotationSpec() {
@Test
suspend fun testEventListenerWasNotified() {
val context = ApplicationContext.run()
val emitter = context.getBean(SampleEventEmitterBean::class.java)
val listener = context.getBean(SampleEventListener::class.java)
listener.invocationCounter.get().shouldBe(0)
emitter.publishSampleEvent()
eventually(5.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) {
println("Current value of counter: " + listener.invocationCounter.get())
listener.invocationCounter.get().shouldBe(1)
}
context.close()
}
}
默认情况下,事件侦听器在计划的执行器上运行。你可以在 application.yml
中根据需要配置此线程池:
配置计划任务线程池
micronaut:
executors:
scheduled:
type: scheduled
core-pool-size: 30
3.14 Bean 事件
你可以使用以下接口之一钩住 bean 的创建:
- BeanInitializedEventListener ——允许在设置属性之后、但在
@PostConstruct
事件钩子之前修改或替换 bean。 - BeanCreatedEventListener ——允许在 bean 完全初始化并调用所有
@PostConstruct
钩子后修改或替换 bean。
BeanInitializedEventListener
接口通常与 Factory bean 结合使用。考虑以下示例:
- Java
- Groovy
- Kotlin
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;
}
}
class V8Engine implements Engine {
final int cylinders = 8
double rodLength // (1)
@Override
String start() {
return "Starting V$cylinders [rodLength=$rodLength]"
}
}
@Factory
class EngineFactory {
private V8Engine engine
double rodLength = 5.7
@PostConstruct
void initialize() {
engine = new V8Engine(rodLength: rodLength) // (2)
}
@Singleton
Engine v8Engine() {
return engine // (3)
}
}
@Singleton
class EngineInitializer implements BeanInitializedEventListener<EngineFactory> { // (4)
@Override
EngineFactory onInitialized(BeanInitializingEvent<EngineFactory> event) {
EngineFactory engineFactory = event.bean
engineFactory.rodLength = 6.6 // (5)
return engineFactory
}
}
class V8Engine(var rodLength: Double) : Engine { // (1)
override val cylinders = 8
override fun start(): String {
return "Starting V$cylinders [rodLength=$rodLength]"
}
}
@Factory
class EngineFactory {
private var engine: V8Engine? = null
private var rodLength = 5.7
@PostConstruct
fun initialize() {
engine = V8Engine(rodLength) // (2)
}
@Singleton
fun v8Engine(): Engine? {
return engine// (3)
}
fun setRodLength(rodLength: Double) {
this.rodLength = rodLength
}
}
@Singleton
class EngineInitializer : BeanInitializedEventListener<EngineFactory> { // (4)
override fun onInitialized(event: BeanInitializingEvent<EngineFactory>): EngineFactory {
val engineFactory = event.bean
engineFactory.setRodLength(6.6) // (5)
return engineFactory
}
}
V8Engine
类定义了rodLength
属性EngineFactory
初始化rodLength
的值并创建实例- 创建的实例作为 Bean 返回
- 实现
BeanInitializedEventListener
接口以监听工厂的初始化 - 在
onInitialized
方法中,rodLength
在工厂 bean 创建引擎之前被重写。
BeanCreatedEventListener 接口通常用于修饰或增强完全初始化的 bean,例如通过创建代理。
Bean 事件监听器在类型转换器前初始化。如果事件监听器通过依赖配置属性 bean 或任何其他机制依赖类型转换,则可能会看到与类型转换相关的错误。
3.15 Bean 自省
从 Micronaut 1.1 开始,JDK 的 Introspector 类就包含了编译时替换。
BeanIntrospector 和 BeanIntrospection 接口允许查找 bean 自省来实例化和读/写 bean 属性,而无需使用反射或缓存反射元数据,因为反射元数据会为大型 bean 消耗过多内存。
让 Bean 可供自省
与 JDK 的 Introspector 不同,不是每个类都可以自动进行自省。要使一个类可用于自省,你必须在构建中至少启用 Micronaut 的注解处理器(micronaut-inject-java
用于 Java 和 Kotlin,micronaut-inject-groovy
用于 Groovy),并确保有依赖 micronaut-core
的运行时。
- Gradle
- Maven
annotationProcessor("io.micronaut:micronaut-inject-java:3.9.4")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>3.9.4</version>
</path>
</annotationProcessorPaths>
对于 Kotlin,在 kapt
范围中添加 micronaut-inject-java
依赖,对于 Groovy,在 compileOnly
范围中添加 micronaut-inject-groovy
。
- Gradle
- Maven
runtimeOnly("io.micronaut:micronaut-core:3.9.4")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-core</artifactId>
<version>3.9.4</version>
<scope>runtime</scope>
</dependency>
使用 @Introspected
注解
@Introspected 注解可用于任何类,以使其可用于自省。简单使用 @Introspected 注解类:
- Java
- Groovy
- Kotlin
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;
}
}
import groovy.transform.Canonical
import io.micronaut.core.annotation.Introspected
@Introspected
@Canonical
class Person {
String name
int age = 18
Person(String name) {
this.name = name
}
}
import io.micronaut.core.annotation.Introspected
@Introspected
data class Person(var name : String) {
var age : Int = 18
}
一旦在编译时生成了自省数据,就可以通过 BeanTranspection API 检索它:
- Java
- Groovy
- Kotlin
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());
def introspection = BeanIntrospection.getIntrospection(Person) // (1)
Person person = introspection.instantiate("John") // (2)
println("Hello $person.name")
BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String) // (3)
property.set(person, "Fred") // (4)
String name = property.get(person) // (5)
println("Hello $person.name")
val introspection = BeanIntrospection.getIntrospection(Person::class.java) // (1)
val person : Person = introspection.instantiate("John") // (2)
print("Hello ${person.name}")
val property : BeanProperty<Person, String> = introspection.getRequiredProperty("name", String::class.java) // (3)
property.set(person, "Fred") // (4)
val name = property.get(person) // (5)
print("Hello ${person.name}")
- 你可以使用静态
getIntrospection
方法检索 BeanIntrospection - 一旦有了 BeanIntrospection,就可以使用
instantiate
方法实例化一个 bean。 - 可以从自省中检索 BeanProperty
- 使用
set
方法设置属性值 - 使用
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;
}
}
- 用
@AccessorsStyle
注解类,为 getter 和 setter 定义空的读写前缀。 - 定义不带前缀的 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 中定义带有公共或包保护字段的类:
- Java
- Groovy
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;
}
}
import io.micronaut.core.annotation.Introspected
@Introspected(accessKind = Introspected.AccessKind.FIELD)
class User {
public final String name // (1)
public int age = 18 // (2)
User(String name) {
this.name = name
}
}
- final 字段被视为只读属性
- 可变字段被视为读写属性
accessKind
接受一个数组,因此可以允许两种类型的访问器,但根据它们在注解中出现的顺序,更喜欢其中一种。列表中的第一个具有优先级。
在 Kotlin 中无法对字段进行自省,因为无法直接声明字段。
构造方法
对于具有多个构造函数的类,将 @Creator 注解应用于要使用的构造函数。
- Java
- Groovy
- Kotlin
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;
}
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected
import javax.annotation.concurrent.Immutable
@Introspected
@Immutable
class Vehicle {
final String make
final String model
final int axles
Vehicle(String make, String model) {
this(make, model, 2)
}
@Creator // (1)
Vehicle(String make, String model, int axles) {
this.make = make
this.model = model
this.axles = axles
}
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected
import javax.annotation.concurrent.Immutable
@Introspected
@Immutable
class Vehicle @Creator constructor(val make: String, val model: String, val axles: Int) { // (1)
constructor(make: String, model: String) : this(make, model, 2) {}
}
- @Creator 注解表示要使用的构造函数
该类没有默认构造函数,因此在没有参数的情况下调用实例化会引发 InstantiationException。
静态 Creator 方法
@Creator 注解可以应用于创建类实例的静态方法。
- Java
- Groovy
- Kotlin
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;
}
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected
import javax.annotation.concurrent.Immutable
@Introspected
@Immutable
class Business {
final String name
private Business(String name) {
this.name = name
}
@Creator // (1)
static Business forName(String name) {
new Business(name)
}
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected
import javax.annotation.concurrent.Immutable
@Introspected
@Immutable
class Business private constructor(val name: String) {
companion object {
@Creator // (1)
fun forName(name: String): Business {
return Business(name)
}
}
}
- @Creator 注解应用于实例化类的静态方法
可以注解多个“creator”方法。如果有一个没有参数,它将是默认的构造方法。第一个带参数的方法将用作主要构造方法。
枚举
也可以对枚举进行自省。将注解添加到枚举中,它可以通过标准 valueOf
方法构造。
在配置类中使用 @Introspected
如果要自省的类已经编译并且不在你的控制之下,另一种选择是使用 @Introspected 注解集的 classes
成员定义一个配置类。
- Java
- Groovy
- Kotlin
import io.micronaut.core.annotation.Introspected;
@Introspected(classes = Person.class)
public class PersonConfiguration {
}
import io.micronaut.core.annotation.Introspected
@Introspected(classes = Person)
class PersonConfiguration {
}
import io.micronaut.core.annotation.Introspected
@Introspected(classes = [Person::class])
class PersonConfiguration
在上面的示例中,PersonConfiguration
类为 Person
类生成自省。
你还可以使用 @Introspected 的 packages
成员,该包在编译时扫描并为包中的所有类生成自省。注意,此功能目前被视为实验性。
编写 AnnotationMapper 以自省现有注解
如果默认情况下你希望自省现有注解,则可以编写 AnnotationMap。
这方面的一个例子是 EntityIntrospectedAnnotationMap,它确保所有用 javax.persistence.Entity
注解的 bean 在默认情况下都是可自省的。
AnnotationMap
必须位于注解处理器 classpath 上。
BeanWrapper API
BeanProperty 提供了读取和写入给定类的属性值的原始访问,不提供任何自动类型转换。
传递给 set
和 get
方法的值应与基础属性类型匹配,否则将发生异常。
为了提供额外的类型转换智能,BeanWrapper 接口允许包装现有 bean 实例,设置并获取 bean 的属性,并根据需要执行类型转换。
- Java
- Groovy
- Kotlin
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);
final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")) // (1)
wrapper.setProperty("age", "20") // (2)
int newAge = wrapper.getRequiredProperty("age", Integer) // (3)
println("Person's age now $newAge")
val wrapper = BeanWrapper.getWrapper(Person("Fred")) // (1)
wrapper.setProperty("age", "20") // (2)
val newAge = wrapper.getRequiredProperty("age", Int::class.java) // (3)
println("Person's age now $newAge")
- 使用静态
getWrapper
方法获取 bean 实例的 BeanWrapper。 - 你可以设置财产,BeanWrapper 将执行类型转换,如果转换不可能,则抛出 ConversionErrorException。
- 你可以使用
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
模块作为编译依赖项:
- Gradle
- Maven
implementation("io.micronaut:micronaut-validation")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</dependency>
注意,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 的实现。
- Gradle
- Maven
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
<groupId>io.micronaut.beanvalidation</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
校验 Bean 方法
通过对参数应用 javax.validation
注解,可以验证任何声明为 Micronaut bean 的类的方法:
校验方法
- Java
- Groovy
- Kotlin
import jakarta.inject.Singleton;
import javax.validation.constraints.NotBlank;
@Singleton
public class PersonService {
public void sayHello(@NotBlank String name) {
System.out.println("Hello " + name);
}
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
class PersonService {
void sayHello(@NotBlank String name) {
println "Hello $name"
}
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
open class PersonService {
open fun sayHello(@NotBlank name: String) {
println("Hello $name")
}
}
上面的示例声明调用 sayHello
方法时将验证 @NotBlank
注解。
如果使用 Kotlin,则必须将类和方法声明为 open
的,这样 Micronaut 才能创建编译时子类。或者,你可以使用 @Validated 注解类,并将 Kotlin all-open
插件配置为使用此类型注解的类。参阅编译器插件部分。
如果发生验证错误,将引发 javax.validation.ConstraintViolationException
。例如:
ConstraintViolationException 示例
- Java
- Groovy
- Kotlin
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)
}
}
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import jakarta.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec extends Specification {
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with a blank string"
personService.sayHello("") // (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "sayHello.name: must not be blank" // (2)
}
}
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import jakarta.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec {
@Inject
lateinit var personService: PersonService
@Test
fun testThatNameIsValidated() {
val exception = assertThrows(ConstraintViolationException::class.java) {
personService.sayHello("") // (1)
}
assertEquals("sayHello.name: must not be blank", exception.message) // (2)
}
}
- 方法使用空串调用
- 异常出现
校验数据类
要验证数据类,例如 POJO(通常用于 JSON 交换),必须用 @Introspected 注解该类(参阅前面关于 Bean 自省 的章节),或者,如果该类是外部的,则通过 @Introsspected
注解导入。
POJO 校验示例
- Java
- Groovy
- Kotlin
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;
}
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
class Person {
@NotBlank
String name
@Min(18L)
int age
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
data class Person(
@field:NotBlank var name: String,
@field:Min(18) var age: Int
)
@Introspected 注解可以用作元注解;像 @javax.persistence.Entity
这样的常见注解被视为 @Introspected
上面的示例定义了一个 Person
类,该类有两个应用了约束的属性(name
和 age
)。注意,在 Java 中,注解可以位于字段或 getter 上,对于 Kotlin 数据类,注解应该以字段为目标。
要手动验证类,请注入 Validator 的实例:
手动校验示例
- Java
- Groovy
- Kotlin
@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)
}
@Inject Validator validator
void "test person is validated with validator"() {
when:"The person is validated"
def constraintViolations = validator.validate(new Person(name: "", age: 10)) // (1)
then:"A validation error occurs"
constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var validator: Validator
@Test
fun testThatPersonIsValidWithValidator() {
val person = Person("", 10)
val constraintViolations = validator.validate(person) // (1)
assertEquals(2, constraintViolations.size) // (2)
}
- 验证器验证 person
- 验证约束冲突
或者,在 Bean 方法上,你可以使用 javax.validation.Valid
来触发级联验证:
ConstraintViolationException 示例
- Java
- Groovy
- Kotlin
@Singleton
public class PersonService {
public void sayHello(@Valid Person person) {
System.out.println("Hello " + person.getName());
}
}
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with an invalid person"
personService.sayHello(new Person(name: "", age: 10)) // (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var personService: PersonService
@Test
fun testThatPersonIsValid() {
val person = Person("", 10)
val exception = assertThrows(ConstraintViolationException::class.java) {
personService.sayHello(person) // (1)
}
assertEquals(2, exception.constraintViolations.size) // (2)
}
- 已调用验证的方法
- 验证约束冲突
校验配置属性
你还可以验证用 @ConfigurationProperties 注解的类的属性,以确保配置正确。
建议你使用 @Context 注解具有验证功能的 @ConfigurationProperties,以确保在启动时进行验证
定义额外约束
要定义额外约束,请创建新注解,例如:
约束注解示例
- Java
- Groovy
- Kotlin
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)
}
}
import javax.validation.Constraint
import java.lang.annotation.Retention
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Retention(RUNTIME)
@Constraint(validatedBy = []) // (1)
@interface DurationPattern {
String message() default "invalid duration ({validatedValue})" // (2)
}
import javax.validation.Constraint
import kotlin.annotation.AnnotationRetention.RUNTIME
@Retention(RUNTIME)
@Constraint(validatedBy = []) // (1)
annotation class DurationPattern(
val message: String = "invalid duration ({validatedValue})" // (2)
)
- 注解应使用
javax.validationConstraint
进行注解 - 可以按如上所述的硬编码方式提供
message
模板。如果未指定,Micronaut 将尝试使用 MessageSource 接口(可选)使用ClassName.message
查找 message - 为了支持重复注解,可以定义内部注解(可选)
可以使用 MessageSource 和 ResourceBundleMessageSource 类添加消息和消息束。参阅资源捆绑包文档。
定义注解后,请实现一个用于验证注解的 ConstraintValidator。你可以创建一个直接实现接口的 bean 类,也可以定义一个返回一个或多个验证器的工厂。
如果你计划定义多个验证器,建议使用后一种方法:
约束校验器示例
- Java
- Groovy
- Kotlin
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}$");
};
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
import jakarta.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return { CharSequence value,
AnnotationValue<DurationPattern> annotation,
ConstraintValidatorContext context ->
context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // (1)
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
} as ConstraintValidator<DurationPattern, CharSequence>
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import jakarta.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
fun durationPatternValidator() : ConstraintValidator<DurationPattern, CharSequence> {
return ConstraintValidator { value, annotation, context ->
context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // (1)
value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
}
- 使用内联调用重写默认消息模板,以获得对验证错误消息的更多控制。(自
2.5.0
起)
上面的示例实现了一个验证器,它验证用 DurationPattern
注解的任何字段、参数等,确保可以使用 java.time.Duration.parse
解析字符串。
通常,null
被视为有效,@NotNull
用于约束值不为 null
。上面的示例将 null
视为有效值。
例如:
自定义约束使用示例
- Java
- Groovy
- Kotlin
@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;
}
}
@Singleton
class HolidayService {
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"
}
}
@Singleton
open class HolidayService {
open fun startHoliday(@NotBlank person: String,
@DurationPattern duration: String): String {
val d = Duration.parse(duration)
return "Person $person is off on holiday for ${d.toMinutes()} minutes"
}
}
要验证上述示例是否验证 duration
参数,请定义测试:
自定义约束使用测试示例
- Java
- Groovy
- Kotlin
@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);
}
void "test test custom validator"() {
when:"A custom validator is used"
holidayService.startHoliday("Fred", "junk") // (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "startHoliday.duration: invalid duration (junk), additional custom message" // (2)
}
@Inject
lateinit var holidayService: HolidayService
@Test
fun testCustomValidator() {
val exception = assertThrows(ConstraintViolationException::class.java) {
holidayService.startHoliday("Fred", "junk") // (1)
}
assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) // (2)
}
- 已调用验证的方法
- 验证了约束冲突
编译时校验注解
你可以使用 Micronaut 的验证器在编译时验证注解元素,方法是在注解处理器 classpath 中包含 micronaut-validation
:
- Gradle
- Maven
annotationProcessor("io.micronaut:micronaut-validation")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</path>
</annotationProcessorPaths>
然后,Micronaut 将在编译时验证自己用 javax.validation
注解的注解值。例如,考虑以下注解:
注解校验
- Java
- Groovy
- Kotlin
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
public @interface TimeOff {
@DurationPattern
String duration();
}
import java.lang.annotation.Retention
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Retention(RUNTIME)
@interface TimeOff {
@DurationPattern
String duration()
}
import kotlin.annotation.AnnotationRetention.RUNTIME
@Retention(RUNTIME)
annotation class TimeOff(
@DurationPattern val duration: String
)
如果你尝试在源代码中使用 @TimeOff(duration="junk")
,Micronaut 将因 duration
值违反 DurationPattern
约束而导致编译失败。
如果 duration
是一个属性占位符,例如 @TimeOff(duration="${my.value}")
,则验证将延迟到运行时。
请注意,要在编译时使用自定义 ConstraintValidator
,必须将验证器定义为类:
约束校验器示例
- Java
- Groovy
- Kotlin
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}$");
}
}
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
class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
}
}
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
class DurationPatternValidator : ConstraintValidator<DurationPattern, CharSequence> {
override fun isValid(
value: CharSequence?,
annotationMetadata: AnnotationValue<DurationPattern>,
context: ConstraintValidatorContext): Boolean {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
此外:
- 定义引用类的
META-INF/service/io.micronaut.validation.validator.constraints.ConstraintValidator
文件。 - 类必须是公共的,并且具有公共的无参数构造函数
- 该类必须位于要验证的项目的注解处理器 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 的不同之处在于,它将这些规则扩展到方法和方法参数,从而:
- 任何用 Inherited 注解并出现在被子接口或类
B
覆盖的接口或超类A
的方法上的注解,都将被继承到 AnnotationMetadata 中,该 AnnotationMetadata 可通过 ExecutableMethod API 从 BeanDefinition 或 AOP 拦截器中检索。 - 任何用 Inherited 注解并出现在被子接口或类
B
重写的接口或超类A
的方法参数上的注解,都将被继承到 AnnotationMetadata 中,该 AnnotationMetadata 可通过 Argument 接口从 ExecutableMethod API 的getArguments
方法检索。
通常情况下,你可能希望覆盖的行为不会默认继承,包括 Bean 作用域、Bean 限定符、Bean 条件、验证规则等。
如果你希望在子类化时继承特定范围、限定符或一组要求,那么可以定义一个用 @inherited
注解的元注解。例如:
定义继承元数据
- Java
- Groovy
- Kotlin
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 "";
}
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)
@interface SqlRepository {
@AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
String value() default "";
}
import io.micronaut.context.annotation.Requires
import jakarta.inject.Named
import jakarta.inject.Singleton
import java.lang.annotation.Inherited
@Inherited // (1)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
annotation class SqlRepository(
val value: String = ""
)
有了这个元注解,你可以将注解添加到超类中:
在超类中使用继承元注解
- Java
- Groovy
- Kotlin
@SqlRepository
public abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository
然后,子类将继承所有注解:
在子类中继承注解
- Java
- Groovy
- Kotlin
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;
}
}
import jakarta.inject.Named
import javax.sql.DataSource
@Named("bookRepository")
class BookRepository extends BaseSqlRepository {
private final DataSource dataSource
BookRepository(DataSource dataSource) {
this.dataSource = dataSource
}
}
import jakarta.inject.Named
import javax.sql.DataSource
@Named("bookRepository")
class BookRepository(private val dataSource: DataSource) : BaseSqlRepository()
子类必须至少有一个 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 "";
}
value
成员也设置id
成员id
成员也设置value
成员
有了这些别名,无论你是定义 @Client("foo")
或 @Client(id="foo")
,都将设置 value
和 id
成员,从而更容易解析和处理注解。
如果无法控制注解,另一种方法是使用 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()
);
}
}
map
方法接收带有注解值的 AnnotationValue。- 可以返回一个或多个注解,在本例中为
@Transient
。
上面的示例实现了 NamedAnnotationMapper 接口,该接口允许注解与运行时代码混合。要针对具体的注解类型进行操作,请改用 TypedAnnotationMapper,尽管注意它需要注解类本身位于注解处理器 classpath。
3.18 从库导入 Bean
你可以使用 @Import 注解从使用 JSR-330 注解的外部已编译库中导入 bean。
Bean 导入目前仅在 Java 语言中受支持,因为其他语言在源代码处理期间对 classpath 扫描有限制。
例如,要将 JSR-330 TCK 导入应用程序,请添加对 TCK 的依赖:
- Gradle
- Maven
implementation("io.micronaut:jakarta.inject")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>jakarta.inject</artifactId>
</dependency>
然后在 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 {
}
- 定义 @Import
- 定义了要导入的包。请注意,Micronaut 不会通过子包递归,因此需要显式列出子包
- 默认情况下,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.Nullable
和 javax.anneration.Nonnull
。
为什么要在代码中使用可为 null 注解?
它使你的代码更容易从 Kotlin 使用。当你从 Kotlin 代码调用 Java 代码时,Kotlin 识别可为 null 注解,并将根据它们的注解处理类型。
此外,你可以使用 @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 构建配置的 annotationProcessor
和 compileOnly
范围中包含 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
});
}
}
- 继承
android.app.Application
类 - 使用
ANDROID
环境运行ApplicationContext
- 注册
ActivityLifecycleCallbacks
实例以允许 AndroidActivity
实例的依赖注入