上下文和依赖注入
Quarkus DI 解决方案(也称为 ArC)基于 Jakarta Contexts and Dependency Injection 4.1 规范。它实现了 CDI Lite 规范,并在其基础上进行了一些改进,并通过了 CDI Lite TCK。它没有实现 CDI Full。另请参阅支持的功能和限制列表。大多数现有的 CDI 代码应该可以正常工作,但由于 Quarkus 架构和目标的原因,存在一些小的差异。
如果您是 CDI 的新手,我们建议您首先阅读CDI 简介。 |
CDI 集成指南包含有关常见 CDI 相关集成用例的更多详细信息,以及解决方案的示例代码。 |
1. Bean 发现
CDI 中的 Bean 发现是一个复杂的过程,涉及遗留的部署结构和底层模块架构的可访问性要求。但是,Quarkus 使用的是简化的 Bean 发现。只有一个 Bean 存档,其Bean 发现模式为 annotated
,并且没有可见性边界。
Bean 存档由以下内容合成:
-
应用程序类,
-
包含
beans.xml
描述符的依赖项(内容被忽略), -
包含 Jandex 索引的依赖项 -
META-INF/jandex.idx
, -
在
application.properties
中由quarkus.index-dependency
引用的依赖项, -
和 Quarkus 集成代码。
没有Bean 定义注解的 Bean 类不会被发现。此行为由 CDI 定义。但是,即使声明类没有使用 Bean 定义注解进行注释,也会发现生产者方法和字段以及观察者方法(此行为与 CDI 中定义的行为不同)。实际上,声明 Bean 类被视为使用 @Dependent
注释。
Quarkus 扩展可能会声明其他发现规则。例如,即使声明类没有使用 Bean 定义注解进行注释,也会注册 @Scheduled 业务方法。 |
1.1. 如何生成 Jandex 索引
具有 Jandex 索引的依赖项会自动扫描 Bean。要生成索引,只需将以下插件添加到您的构建文件中
如果您无法修改依赖项,您仍然可以通过将 quarkus.index-dependency
条目添加到您的 application.properties
来索引它
quarkus.index-dependency.<name>.group-id=
quarkus.index-dependency.<name>.artifact-id=(this one is optional)
quarkus.index-dependency.<name>.classifier=(this one is optional)
如果未指定 artifact-id ,则将索引具有指定 group-id 的所有依赖项。 |
例如,以下条目确保 org.acme:acme-api
依赖项被索引
quarkus.index-dependency.acme.group-id=org.acme (1)
quarkus.index-dependency.acme.artifact-id=acme-api (2)
1 | 该值是名称为 acme 的依赖项的组 ID。 |
2 | 该值是名称为 acme 的依赖项的构件 ID。 |
1.2. 如何从发现中排除类型和依赖项
有时,来自第三方库的某些 Bean 在 Quarkus 中无法正常工作。一个典型的例子是注入便携式扩展的 Bean。在这种情况下,可以从 Bean 发现中排除类型和依赖项。quarkus.arc.exclude-types
属性接受一个字符串值列表,这些值用于匹配应该排除的类。
值 |
描述 |
|
匹配类的完全限定名称 |
|
匹配具有包 |
|
匹配包以 |
|
匹配类的简单名称 |
quarkus.arc.exclude-types=org.acme.Foo,org.acme.*,Bar (1)(2)(3)
1 | 排除类型 org.acme.Foo 。 |
2 | 排除 org.acme 包中的所有类型。 |
3 | 排除所有简单名称为 Bar 的类型 |
也可以排除依赖项构件,否则会扫描该构件以查找 Bean。例如,因为它包含 beans.xml
描述符。
quarkus.arc.exclude-dependency.acme.group-id=org.acme (1)
quarkus.arc.exclude-dependency.acme.artifact-id=acme-services (2)
1 | 该值是名称为 acme 的依赖项的组 ID。 |
2 | 该值是名称为 acme 的依赖项的构件 ID。 |
2. 基于字符串的限定符
您可能熟悉的 @Named
限定符是一个基于字符串的限定符。也就是说,限定符注解的字符串值决定了限定符是否匹配。这是不类型安全的,不应该是 CDI 应用程序中的常态。应首选特定的限定符类型。
但是,有时基于字符串的限定符是必要的。在这种情况下,请避免使用 @Named
限定符,因为在 CDI 中,它的工作方式与所有其他限定符不同。
具体来说:如果 Bean 唯一的限定符是 @Named
,那么它也会自动获得 @Default
。这意味着如果存在相同类型的多个 Bean,其中一个没有限定符,而其他 Bean 具有 @Named
,那么它们都会获得 @Default
限定符,并且 Bean 解析会因歧义而报错。例如
@ApplicationScoped
public class Producers {
@Produces
MyBean produce() {
...
}
@Produces
@Named("foo")
MyBean produceFoo() {
...
}
}
@ApplicationScoped
public class Consumer {
@Inject
MyBean bean;
}
在这种情况下,Consumer#bean
注入点将导致歧义错误,因为两个 MyBean
生产者都将具有 @Default
限定符。
可以使用 @io.smallrye.common.annotation.Identifier
代替 @Named
。这是一个常规限定符,其工作方式与其他限定符类似。因此,如果我们将示例重写为使用 @Identifier
@ApplicationScoped
public class Producers {
@Produces
MyBean produce() {
...
}
@Produces
@Identifier("foo")
MyBean produceFoo() {
...
}
}
@ApplicationScoped
public class Consumer {
@Inject
MyBean bean;
}
只有第一个生产者会获得 @Default
限定符,第二个不会。因此,不会有错误,一切都会按预期工作。
2.1. 何时使用 @Named
?
在一种情况下,使用 @Named
是正确的:为不支持直接依赖注入的其他语言指定外部标识符。
例如
@ApplicationScoped
@Named("myBean")
public class MyBean {
public String getValue() {
...
}
}
@ApplicationScoped
public class Consumer {
@Inject
MyBean bean;
}
如您所见,在应用程序代码中,Bean 是在没有限定符的情况下注入的。Bean 名称仅用于在其他语言中引用 Bean。
从历史上看,使用 Bean 名称最常见的外部语言是 JSF。在 Quarkus 中,我们有Qute。在 Qute 模板中,可以使用其名称引用 Bean
The current value is {inject:myBean.value}.
在此用例之外,只需使用 @Identifier
。
3. 本地可执行文件和私有成员
Quarkus 使用 GraalVM 构建本地可执行文件。GraalVM 的一个限制是反射的使用。支持反射操作,但所有相关成员必须显式注册以进行反射。这些注册会导致更大的本地可执行文件。
如果 Quarkus DI 需要访问私有成员,则必须使用反射。这就是为什么鼓励 Quarkus 用户不要在他们的 Bean 中使用私有成员的原因。这包括注入字段、构造函数和初始化程序、观察者方法、生产者方法和字段、处置者和拦截器方法。
如何避免使用私有成员?您可以使用包私有修饰符
@ApplicationScoped
public class CounterBean {
@Inject
CounterService counterService; (1)
void onMessage(@Observes Event msg) { (2)
}
}
1 | 包私有注入字段。 |
2 | 包私有观察者方法。 |
或者构造函数注入
@ApplicationScoped
public class CounterBean {
private CounterService service;
CounterBean(CounterService service) { (1)
this.service = service;
}
}
1 | 包私有构造函数注入。在这种特殊情况下,@Inject 是可选的。 |
4. 支持的功能和限制
完全支持 CDI Lite 规范。还支持 CDI Full 中的以下功能
-
装饰器
-
不支持内置 Bean(如
Event
)的装饰
-
-
BeanManager
-
除了
BeanContainer
方法外,还支持以下方法:getInjectableReference()
、resolveDecorators()
-
-
@SessionScoped
-
仅适用于 Undertow 扩展;有关详细信息,请参阅此处
-
方法调用器实现支持异步方法。以下方法被认为是异步的,并且只有在异步操作完成后才会销毁 @Dependent
实例
-
声明
CompletionStage
、Uni
或Multi
返回类型的方法
这些附加功能不在 CDI Lite TCK 的范围内。 |
5. 非标准功能
5.1. Bean 的急切实例化
5.1.1. 默认情况下延迟
默认情况下,CDI Bean 是在需要时延迟创建的。 “需要”的确切含义取决于 Bean 的作用域。
-
普通作用域 Bean(
@ApplicationScoped
、@RequestScoped
等)是在注入实例(规范中每个上下文引用的上下文引用)上调用方法时需要的。换句话说,注入普通作用域 Bean 是不够的,因为会注入客户端代理而不是 Bean 的上下文实例。
-
具有伪作用域的 Bean(
@Dependent
和@Singleton
)是在注入时创建的。
@Singleton // => pseudo-scope
class AmazingService {
String ping() {
return "amazing";
}
}
@ApplicationScoped // => normal scope
class CoolService {
String ping() {
return "cool";
}
}
@Path("/ping")
public class PingResource {
@Inject
AmazingService s1; (1)
@Inject
CoolService s2; (2)
@GET
public String ping() {
return s1.ping() + s2.ping(); (3)
}
}
1 | 注入触发 AmazingService 的实例化。 |
2 | 注入本身不会导致 CoolService 的实例化。注入了一个客户端代理。 |
3 | 在注入的代理上第一次调用会触发 CoolService 的实例化。 |
5.1.2. 启动事件
但是,如果您确实需要急切地实例化 Bean,您可以
-
声明
StartupEvent
的观察者 - 在这种情况下,Bean 的作用域无关紧要@ApplicationScoped class CoolService { void startup(@Observes StartupEvent event) { (1) } }
1 CoolService
在启动期间创建,以服务观察者方法调用。 -
在
StartupEvent
的观察者中使用 Bean - 普通作用域 Bean 必须按照默认情况下延迟中的描述使用@Dependent class MyBeanStarter { void startup(@Observes StartupEvent event, AmazingService amazing, CoolService cool) { (1) cool.toString(); (2) } }
1 AmazingService
在注入期间创建。2 CoolService
是一个普通作用域 Bean,因此我们必须在注入的代理上调用一个方法才能强制实例化。 -
使用
@io.quarkus.runtime.Startup
注释 Bean,如启动注解中所述@Startup (1) @ApplicationScoped public class EagerAppBean { private final String name; EagerAppBean(NameGenerator generator) { (2) this.name = generator.createName(); } }
1 对于每个使用 @Startup
注释的 Bean,都会生成一个StartupEvent
的合成观察者。使用默认优先级。2 在应用程序启动时调用 Bean 构造函数,并将生成的上下文实例存储在应用程序上下文中。
如应用程序初始化和终止指南中所述,建议 Quarkus 用户始终优先选择 @Observes StartupEvent 而不是 @Initialized(ApplicationScoped.class) 。 |
5.2. 请求上下文生命周期
请求上下文也是活动的
-
在同步观察者方法的通知期间。
请求上下文已销毁
-
如果在通知开始时它尚未处于活动状态,则在观察者通知完成事件后。
当为观察者通知初始化请求上下文时,会触发具有限定符 @Initialized(RequestScoped.class) 的事件。此外,当请求上下文被销毁时,会触发具有限定符 @BeforeDestroyed(RequestScoped.class) 和 @Destroyed(RequestScoped.class) 的事件。 |
5.3. 限定的注入字段
在 CDI 中,如果您声明一个字段注入点,则需要使用 @Inject
以及可选的一组限定符。
@Inject
@ConfigProperty(name = "cool")
String coolProperty;
在 Quarkus 中,如果注入的字段声明了至少一个限定符,您可以完全跳过 @Inject
注解。
@ConfigProperty(name = "cool")
String coolProperty;
除了下面讨论的一个特殊情况外,构造函数和方法注入仍然需要 @Inject 。 |
5.4. 简化的构造函数注入
在 CDI 中,普通作用域 Bean 必须始终声明一个无参数构造函数(除非您声明任何其他构造函数,否则此构造函数通常由编译器生成)。但是,此要求使构造函数注入变得复杂 - 您需要提供一个虚拟的无参数构造函数才能使 CDI 中的工作正常进行。
@ApplicationScoped
public class MyCoolService {
private SimpleProcessor processor;
MyCoolService() { // dummy constructor needed
}
@Inject // constructor injection
MyCoolService(SimpleProcessor processor) {
this.processor = processor;
}
}
无需在 Quarkus 中为普通作用域 Bean 声明虚拟构造函数 - 它们会自动生成。此外,如果只有一个构造函数,则不需要 @Inject
。
@ApplicationScoped
public class MyCoolService {
private SimpleProcessor processor;
MyCoolService(SimpleProcessor processor) {
this.processor = processor;
}
}
如果 Bean 类扩展了一个未声明无参数构造函数的类,则我们不会自动生成无参数构造函数。 |
5.5. 删除未使用的 Bean
默认情况下,容器会在构建期间尝试删除所有未使用的 Bean、拦截器和装饰器。此优化有助于最大限度地减少生成的类的数量,从而节省内存。但是,Quarkus 无法检测到通过 CDI.current()
静态方法执行的编程查找。因此,删除可能会导致误报错误,即,尽管实际上使用了某个 Bean,但该 Bean 仍被删除。在这种情况下,您会在日志中看到一个很大的警告。用户和扩展作者有几个选项如何消除误报。
可以通过将 quarkus.arc.remove-unused-beans
设置为 none
或 false
来禁用优化。Quarkus 还提供了一个中间方案,其中应用程序 Bean 永远不会被删除,无论它们是否未使用,而优化会正常进行非应用程序类。要使用此模式,请将 quarkus.arc.remove-unused-beans
设置为 fwk
或 framework
。
5.5.1. 什么被删除了?
Quarkus 首先标识所谓的不可删除的 Bean,这些 Bean 构成了依赖树中的根。一个很好的例子是 Jakarta REST 资源类或声明 @Scheduled
方法的 Bean。
不可删除的 Bean
-
是从删除中排除,或者
-
具有通过
@Named
指定的名称,或者 -
声明了一个观察者方法。
未使用的 Bean
-
不是不可删除的,并且
-
不符合注入到不可删除的 Bean 的依赖树中的任何注入点的条件,并且
-
不声明任何符合注入到依赖树中的任何注入点的生产者,并且
-
不符合注入到任何
jakarta.enterprise.inject.Instance
或jakarta.inject.Provider
注入点的条件,并且 -
不符合注入到任何
@Inject @All List<>
注入点的条件。
未使用的拦截器和装饰器不与任何 Bean 相关联。
使用开发模式(运行
|
5.5.2. 如何消除误报
用户可以指示容器不要删除任何特定的 Bean(即使它们满足上面指定的所有规则),方法是使用 @io.quarkus.arc.Unremovable
注释它们。此注解可以在类、生产者方法或字段上声明。
由于这并不总是可能的,因此可以选择通过 application.properties
实现相同的目的。 quarkus.arc.unremovable-types
属性接受一个字符串值列表,这些值用于根据 Bean 的名称或包匹配 Bean。
值 |
描述 |
|
匹配 Bean 类的完全限定名称 |
|
匹配 Bean 类的包为 |
|
匹配 Bean 类的包以 |
|
匹配 Bean 类的简单名称 |
quarkus.arc.unremovable-types=org.acme.Foo,org.acme.*,Bar
此外,扩展可以通过生成 UnremovableBeanBuildItem
来消除误报。
5.6. 默认 Bean
Quarkus 添加了一种 CDI 当前不支持的功能,即如果没有任何可用方法(Bean 类、生产者、合成 Bean 等)声明具有相同类型和限定符的其他 Bean,则有条件地声明一个 Bean。这是使用 @io.quarkus.arc.DefaultBean
注解完成的,最好用一个例子来解释。
假设有一个 Quarkus 扩展,它除了其他事情之外,还声明了一些 CDI Bean,如以下代码所示
@Dependent
public class TracerConfiguration {
@Produces
public Tracer tracer(Reporter reporter, Configuration configuration) {
return new Tracer(reporter, configuration);
}
@Produces
@DefaultBean
public Configuration configuration() {
// create a Configuration
}
@Produces
@DefaultBean
public Reporter reporter(){
// create a Reporter
}
}
这个想法是扩展为用户自动配置,从而消除了大量样板代码 - 我们可以简单地在任何需要的地方 @Inject
一个 Tracer
。现在想象一下,在我们的应用程序中,我们想要利用配置的 Tracer
,但我们需要对其进行一些自定义,例如通过提供自定义的 Reporter
。我们的应用程序中唯一需要的就是以下内容
@Dependent
public class CustomTracerConfiguration {
@Produces
public Reporter reporter(){
// create a custom Reporter
}
}
@DefaultBean
允许扩展(或任何其他代码)提供默认值,同时在以任何 Quarkus 支持的方式提供该类型的 Bean 时退避。
默认 Bean 可以选择声明 @jakarta.annotation.Priority
。如果没有定义优先级,则假定为 @Priority(0)
。优先级值用于 Bean 排序,并在类型安全解析期间用于消除多个匹配的默认 Bean 的歧义。
@Dependent
public class CustomizedDefaultConfiguration {
@Produces
@DefaultBean
@Priority(100)
public Configuration customizedConfiguration(){
// create a customized default Configuration
// this will have priority over previously defined default bean
}
}
5.7. 为 Quarkus 构建配置文件启用 Bean
Quarkus 添加了一种 CDI 当前不支持的功能,即通过 @io.quarkus.arc.profile.IfBuildProfile
和 @io.quarkus.arc.profile.UnlessBuildProfile
注解,在启用 Quarkus 构建时配置文件时有条件地启用 Bean。与 @io.quarkus.arc.DefaultBean
结合使用时,这些注解允许为不同的构建配置文件创建不同的 Bean 配置。
例如,假设一个应用程序包含一个名为 Tracer
的 Bean,该 Bean 在测试或开发模式下不需要执行任何操作,但在生产构件中以其正常能力工作。创建此类 Bean 的一种优雅方式如下
@Dependent
public class TracerConfiguration {
@Produces
@IfBuildProfile("prod")
public Tracer realTracer(Reporter reporter, Configuration configuration) {
return new RealTracer(reporter, configuration);
}
@Produces
@DefaultBean
public Tracer noopTracer() {
return new NoopTracer();
}
}
如果相反,需要 Tracer
Bean 也在开发模式下工作,并且仅在测试时默认不执行任何操作,那么 @UnlessBuildProfile
将是理想的选择。代码将如下所示
@Dependent
public class TracerConfiguration {
@Produces
@UnlessBuildProfile("test") // this will be enabled for both prod and dev build time profiles
public Tracer realTracer(Reporter reporter, Configuration configuration) {
return new RealTracer(reporter, configuration);
}
@Produces
@DefaultBean
public Tracer noopTracer() {
return new NoopTracer();
}
}
运行时配置文件对使用 @IfBuildProfile 和 @UnlessBuildProfile 的 Bean 解析绝对没有影响。 |
也可以在构造型上使用 @IfBuildProfile 和 @UnlessBuildProfile 。 |
5.8. 为 Quarkus 构建属性启用 Bean
Quarkus 添加了一种 CDI 当前不支持的功能,即通过 @io.quarkus.arc.properties.IfBuildProperty
和 @io.quarkus.arc.properties.UnlessBuildProperty
注解,在 Quarkus 构建时属性具有或不具有特定值时有条件地启用 Bean。与 @io.quarkus.arc.DefaultBean
结合使用时,这些注解允许为不同的构建属性创建不同的 Bean 配置。
我们上面提到的 Tracer
场景也可以通过以下方式实现
@Dependent
public class TracerConfiguration {
@Produces
@IfBuildProperty(name = "some.tracer.enabled", stringValue = "true")
public Tracer realTracer(Reporter reporter, Configuration configuration) {
return new RealTracer(reporter, configuration);
}
@Produces
@DefaultBean
public Tracer noopTracer() {
return new NoopTracer();
}
}
@IfBuildProperty 和 @UnlessBuildProperty 是可重复的注解,即,只有当所有这些注解定义的条件都满足时,才会启用 Bean。 |
如果相反,只有当 some.tracer.enabled
属性不是 false
时才需要使用 RealTracer
Bean,那么 @UnlessBuildProperty
将是理想的选择。代码将如下所示
@Dependent
public class TracerConfiguration {
@Produces
@UnlessBuildProperty(name = "some.tracer.enabled", stringValue = "false")
public Tracer realTracer(Reporter reporter, Configuration configuration) {
return new RealTracer(reporter, configuration);
}
@Produces
@DefaultBean
public Tracer noopTracer() {
return new NoopTracer();
}
}
在运行时设置的属性对使用 @IfBuildProperty 的 Bean 解析绝对没有影响。 |
也可以在构造型上使用 @IfBuildProperty 和 @UnlessBuildProperty 。 |
5.9. 声明选定的替代项
在 CDI 中,可以通过 @Priority
为应用程序全局选择替代 Bean,或者通过使用 beans.xml
描述符为 Bean 存档选择替代 Bean。 Quarkus 具有简化的 Bean 发现,并且 beans.xml
的内容将被忽略。
但是,也可以使用统一配置为应用程序选择替代项。 quarkus.arc.selected-alternatives
属性接受一个字符串值列表,这些值用于匹配替代 Bean。如果任何值匹配,则 Integer#MAX_VALUE
的优先级用于相关 Bean。通过 @Priority
声明或从构造型继承的优先级将被覆盖。
值 |
描述 |
|
匹配 Bean 类的完全限定名称或声明生产者的 Bean 的 Bean 类 |
|
匹配 Bean 类的包为 |
|
匹配 Bean 类的包以 |
|
匹配 Bean 类的简单名称或声明生产者的 Bean 的 Bean 类 |
quarkus.arc.selected-alternatives=org.acme.Foo,org.acme.*,Bar
5.10. 简化的生产者方法声明
在 CDI 中,生产者方法必须始终使用 @Produces
注解。
class Producers {
@Inject
@ConfigProperty(name = "cool")
String coolProperty;
@Produces
@ApplicationScoped
MyService produceService() {
return new MyService(coolProperty);
}
}
在 Quarkus 中,如果生产者方法使用作用域注解、构造型或限定符进行注释,则可以完全跳过 @Produces
注解。
class Producers {
@ConfigProperty(name = "cool")
String coolProperty;
@ApplicationScoped
MyService produceService() {
return new MyService(coolProperty);
}
}
5.11. 静态方法的拦截
拦截器规范明确指出,around-invoke 方法不能声明为静态的。但是,此限制主要是由技术限制驱动的。并且由于 Quarkus 是一个面向构建时的堆栈,允许进行额外的类转换,因此这些限制不再适用。可以使用拦截器绑定来注释非私有静态方法
class Services {
@Logged (1)
static BigDecimal computePrice(long amount) { (2)
BigDecimal price;
// Perform computations...
return price;
}
}
1 | Logged 是一个拦截器绑定。 |
2 | 如果存在与 Logged 关联的拦截器,则会拦截每个方法调用。 |
5.12. 处理“final”类和方法的能力
在普通的 CDI 中,标记为 final
和/或具有 final
方法的类不符合代理创建的条件,这反过来意味着拦截器和普通作用域 Bean 无法正常工作。当尝试将 CDI 与 Kotlin 等替代 JVM 语言一起使用时,这种情况非常常见,在 Kotlin 中,类和方法默认都是 final
。
但是,当 quarkus.arc.transform-unproxyable-classes
设置为 true
(这是默认值)时,Quarkus 可以克服这些限制。
5.13. 容器管理的并发
CDI Bean 没有标准的并发控制机制。尽管如此,Bean 实例可以从多个线程共享和并发访问。在这种情况下,它应该是线程安全的。您可以使用标准的 Java 构造(volatile
、synchronized
、ReadWriteLock
等),或让容器控制并发访问。 Quarkus 为此拦截器绑定提供了 @io.quarkus.arc.Lock
和一个内置拦截器。与拦截 Bean 的上下文实例关联的每个拦截器实例都持有具有非公平排序策略的单独的 ReadWriteLock
。
io.quarkus.arc.Lock 是一个常规的拦截器绑定,因此可以用于任何作用域的任何 Bean。但是,它对于“共享”作用域(例如 @Singleton 和 @ApplicationScoped )尤其有用。 |
import io.quarkus.arc.Lock;
@Lock (1)
@ApplicationScoped
class SharedService {
void addAmount(BigDecimal amount) {
// ...changes some internal state of the bean
}
@Lock(value = Lock.Type.READ, time = 1, unit = TimeUnit.SECONDS) (2) (3)
BigDecimal getAmount() {
// ...it is safe to read the value concurrently
}
}
1 | 在类上声明的 @Lock (映射到 @Lock(Lock.Type.WRITE) )指示容器锁定 Bean 实例,以供任何业务方法的任何调用使用,即客户端具有“独占访问权限”,并且不允许并发调用。 |
2 | @Lock(Lock.Type.READ) 覆盖在类级别指定的值。这意味着除非 Bean 实例被 @Lock(Lock.Type.WRITE) 锁定,否则任意数量的客户端可以并发调用该方法。 |
3 | 您还可以指定“等待时间”。如果在给定的时间内无法获取锁,则会抛出 LockException 。 |
5.14. 可重复的拦截器绑定
Quarkus 对 @Repeatable
拦截器绑定注解的支持有限。
将拦截器绑定到组件时,您可以在方法上声明多个 @Repeatable
注解。不支持在类和构造型上声明的可重复拦截器绑定,因为围绕与拦截器规范的交互存在一些未决问题。将来可能会添加此功能。
例如,假设我们有一个清除缓存的拦截器。相应的拦截器绑定将称为 @CacheInvalidateAll
,并将声明为 @Repeatable
。如果我们想要同时清除两个缓存,我们将添加两次 @CacheInvalidateAll
@ApplicationScoped
class CachingService {
@CacheInvalidateAll(cacheName = "foo")
@CacheInvalidateAll(cacheName = "bar")
void heavyComputation() {
// ...
// some computation that updates a lot of data
// and requires 2 caches to be invalidated
// ...
}
}
这就是拦截器的使用方式。那么创建拦截器呢?
声明拦截器的拦截器绑定时,您可以像往常一样向拦截器类添加多个 @Repeatable
注解。当注解成员是 @Nonbinding
时,这将是无用的,对于 @Cached
注解来说就是这种情况,但在其他情况下很重要。
例如,假设我们有一个拦截器,可以自动将方法调用记录到某些目标。拦截器绑定注解 @Logged
将有一个名为 target
的成员,该成员指定在哪里存储日志。我们的实现可能仅限于控制台日志记录和文件日志记录
@Interceptor
@Logged(target = "console")
@Logged(target = "file")
class NaiveLoggingInterceptor {
// ...
}
可以提供其他拦截器来将方法调用记录到不同的目标。
5.15. 缓存程序化查找的结果
在某些情况下,通过注入的 jakarta.enterprise.inject.Instance
和 Instance.get()
以编程方式获取 Bean 实例是实用的。但是,根据规范,get()
方法必须识别匹配的 Bean 并获取上下文引用。因此,从每次调用 get()
都会返回 @Dependent
Bean 的新实例。此外,此实例是注入的 Instance
的依赖对象。此行为是明确定义的,但可能会导致意外错误和内存泄漏。因此,Quarkus 带有 io.quarkus.arc.WithCaching
注解。使用此注解注释的注入的 Instance
将缓存 Instance#get()
操作的结果。结果是在第一次调用时计算的,并且对于所有后续调用都返回相同的值,即使对于 @Dependent
Bean 也是如此。
class Producer {
AtomicLong nextLong = new AtomicLong();
AtomicInteger nextInt = new AtomicInteger();
@Dependent
@Produces
Integer produceInt() {
return nextInt.incrementAndGet();
}
@Dependent
@Produces
Long produceLong() {
return nextLong.incrementAndGet();
}
}
class Consumer {
@Inject
Instance<Long> longInstance;
@Inject
@WithCaching
Instance<Integer> intInstance;
// this method should always return true
// Producer#produceInt() is only called once
boolean pingInt() {
return intInstance.get().equals(intInstance.get());
}
// this method should always return false
// Producer#produceLong() is called twice per each pingLong() invocation
boolean pingLong() {
return longInstance.get().equals(longInstance.get());
}
}
也可以通过 io.quarkus.arc.InjectableInstance.clearCache() 清除缓存的值。在这种情况下,您需要注入 Quarkus 特定的 io.quarkus.arc.InjectableInstance 而不是 jakarta.enterprise.inject.Instance 。 |
5.16. 以声明方式选择可以通过程序化查找获得的 Bean
有时,缩小可以通过 jakarta.enterprise.inject.Instance
程序化查找获得的 Bean 集是有用的。通常,用户需要根据运行时配置属性选择接口的适当实现。
假设我们有两个实现接口 org.acme.Service
的 Bean。您无法直接注入 org.acme.Service
,除非您的实现声明了一个 CDI 限定符。但是,您可以注入 Instance<Service>
,然后迭代所有实现并手动选择正确的实现。或者,您可以使用 @LookupIfProperty
和 @LookupUnlessProperty
注解。 @LookupIfProperty
指示只有当运行时配置属性与提供的值匹配时才能获得 Bean。另一方面,@LookupUnlessProperty
指示只有当运行时配置属性与提供的值不匹配时才能获得 Bean。
@LookupIfProperty
示例 interface Service {
String name();
}
@LookupIfProperty(name = "service.foo.enabled", stringValue = "true")
@ApplicationScoped
class ServiceFoo implements Service {
public String name() {
return "foo";
}
}
@ApplicationScoped
class ServiceBar implements Service {
public String name() {
return "bar";
}
}
@ApplicationScoped
class Client {
@Inject
Instance<Service> service;
void printServiceName() {
// This will print "bar" if the property "service.foo.enabled" is NOT set to "true"
// If "service.foo.enabled" is set to "true" then service.get() would result in an AmbiguousResolutionException
System.out.println(service.get().name());
}
}
5.17. 对通过程序化查找获得的 Bean 进行排序
如果有多个 Bean 匹配所需的类型和限定符并符合注入条件,则可以迭代(或流式传输)可用的 Bean 实例。由流和迭代器方法返回的 Bean 都按 io.quarkus.arc.InjectableBean#getPriority()
定义的优先级排序。更高的优先级排在前面。如果没有显式声明优先级,则假定为 0。
interface Service {
}
@Priority(100)
@ApplicationScoped
class FirstService implements Service {
}
@Priority(10)
@ApplicationScoped
class SecondService implements Service {
}
@ApplicationScoped
class ThirdService implements Service {
}
@ApplicationScoped
class Client {
@Inject
Instance<Service> serviceInstance;
void printServiceName() {
if(service.isAmbiguous()){
for (Service service : serviceInstance) {
// FirstService, SecondService, ThirdService
}
}
}
}
5.18. 直观地注入多个 Bean 实例
在 CDI 中,可以通过实现 java.lang.Iterable
的 jakarta.enterprise.inject.Instance
注入多个 Bean 实例(又名上下文引用)。但是,这并不是完全直观的。因此,Quarkus 中引入了一种新方法 - 您可以注入一个使用 io.quarkus.arc.All
限定符注释的 java.util.List
。执行查找时,列表中的元素类型用作必需的类型。
@ApplicationScoped
public class Processor {
@Inject
@All
List<Service> services; (1) (2)
}
1 | 注入的实例是明确的 Bean 的上下文引用的不可变列表。 |
2 | 对于此注入点,必需的类型是 Service ,并且未声明其他限定符。 |
该列表按 io.quarkus.arc.InjectableBean#getPriority() 定义的优先级排序。更高的优先级排在前面。一般来说,可以使用 @jakarta.annotation.Priority 注解将优先级分配给类 Bean、生产者方法或生产者字段。 |
如果注入点声明了除了 @All
之外的其他限定符,则使用 @Any
,即,该行为等同于 @Inject @Any Instance<Service>
。
您还可以注入包装在 io.quarkus.arc.InstanceHandle
中的 Bean 实例列表。如果您需要检查相关的 Bean 元数据,这将很有用。
@ApplicationScoped
public class Processor {
@Inject
@All
List<InstanceHandle<Service>> services;
public void doSomething() {
for (InstanceHandle<Service> handle : services) {
if (handle.getBean().getScope().equals(Dependent.class)) {
handle.get().process();
break;
}
}
}
}
类型变量和通配符都不是 @All List<> 注入点的合法类型参数,即,不支持 @Inject @All List<?> all ,并且会导致部署错误。 |
也可以通过 Arc.container().listAll() 方法以编程方式获取所有 Bean 实例句柄的列表。 |
5.19. 忽略方法和构造函数的类级别拦截器绑定
如果托管 bean 在类级别声明了拦截器绑定注解,那么相应的 @AroundInvoke
拦截器将应用于所有业务方法。类似地,相应的 @AroundConstruct
拦截器将应用于 bean 构造函数。
例如,假设我们有一个带有 @Logged
绑定注解的日志拦截器和一个带有 @Traced
绑定注解的跟踪拦截器
@ApplicationScoped
@Logged
public class MyService {
public void doSomething() {
...
}
@Traced
public void doSomethingElse() {
...
}
}
在此示例中,doSomething
和 doSomethingElse
都将被假设的日志拦截器拦截。此外,doSomethingElse
方法将被假设的跟踪拦截器拦截。
现在,如果 @Traced
拦截器也执行了所有必要的日志记录,我们希望跳过此方法的 @Logged
拦截器,但保留它用于所有其他方法。为了实现这一点,您可以为该方法添加 @NoClassInterceptors
注解
@Traced
@NoClassInterceptors
public void doSomethingElse() {
...
}
@NoClassInterceptors
注解可以放在方法和构造函数上,表示所有类级别的拦截器都将被这些方法和构造函数忽略。换句话说,如果一个方法/构造函数被注解为 @NoClassInterceptors
,那么唯一会应用于此方法/构造函数的拦截器是直接在该方法/构造函数上声明的拦截器。
此注解仅影响业务方法拦截器 (@AroundInvoke
) 和构造函数生命周期回调拦截器 (@AroundConstruct
)。
5.20. 异步观察者方法抛出的异常
如果异步观察者抛出异常,则 fireAsync()
方法返回的 CompletionStage
会异常完成,以便事件生产者可以适当地做出反应。但是,如果事件生产者不在意,则该异常会被静默忽略。因此,Quarkus 默认会记录一条错误消息。也可以实现自定义的 AsyncObserverExceptionHandler
。实现此接口的 bean 应该是 @jakarta.inject.Singleton
或 @jakarta.enterprise.context.ApplicationScoped
。
NoopAsyncObserverExceptionHandler
@Singleton
public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {
void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {
// do nothing
}
}
5.21. 被拦截的自调用
Quarkus 支持所谓的被拦截的自调用或简称自拦截 - CDI bean 从另一个方法中调用其自身的被拦截方法,同时触发任何关联的拦截器。这是一个非标准特性,因为 CDI 规范没有定义自拦截是否应该工作。
假设我们有一个 CDI bean,其中两个方法,其中一个方法具有与之关联的 @Transactional
拦截器绑定
@ApplicationScoped
public class MyService {
@Transactional (1)
void doSomething() {
// some application logic
}
void doSomethingElse() {
doSomething();(2)
}
}
1 | 一个或多个拦截器绑定;@Transactional 只是一个例子。 |
2 | 非拦截方法调用同一 bean 中的另一个具有关联绑定方法;这将触发拦截。 |
在上面的例子中,任何调用 doSomething()
方法的代码都会触发拦截 - 在这种情况下,该方法变为事务性的。无论调用是直接来自 MyService
bean(例如 MyService#doSomethingElse
)还是来自其他 bean,都是如此。
5.22. 拦截生产者方法和合成 Bean
默认情况下,拦截仅支持托管 bean(也称为基于类的 bean)。为了支持生产者方法和合成 bean 的拦截,CDI 规范包含了一个 InterceptionFactory
,这是一个面向运行时的概念,因此 Quarkus 无法支持。
相反,Quarkus 有自己的 API:InterceptionProxy
和 @BindingsSource
。InterceptionProxy
非常类似于 InterceptionFactory
:它创建一个代理,该代理在将方法调用转发到目标实例之前应用 @AroundInvoke
拦截器。如果被拦截的类是外部的且无法更改,则 @BindingsSource
注解允许设置拦截器绑定。
import io.quarkus.arc.InterceptionProxy;
@ApplicationScoped
class MyProducer {
@Produces
MyClass produce(InterceptionProxy<MyClass> proxy) { (1)
return proxy.create(new MyClass()); (2)
}
}
1 | 声明 InterceptionProxy<MyClass> 类型的注入点。这意味着在构建时,会生成 MyClass 的子类,该子类执行拦截和转发。请注意,类型参数必须与生产者方法的返回类型相同。 |
2 | 为给定的 MyClass 实例创建拦截代理的实例。方法调用将在所有拦截器运行后转发到此目标实例。 |
在此示例中,拦截器绑定是从 MyClass
类读取的。
请注意,InterceptionProxy 仅支持在拦截器类上声明的 @AroundInvoke 拦截器。不支持其他类型的拦截,以及在目标类及其超类上声明的 @AroundInvoke 拦截器。 |
被拦截的类应该是可代理的,因此不应是 final
,不应具有非私有的 final
方法,并且应具有非私有的零参数构造函数。如果不是,如果启用,字节码转换将尝试修复它,但请注意,添加零参数构造函数并不总是可能的。
通常情况下,生成的类来自外部库,根本不包含拦截器绑定注解。为了支持这种情况,可以在 InterceptionProxy
参数上声明 @BindingsSource
注解
import io.quarkus.arc.BindingsSource;
import io.quarkus.arc.InterceptionProxy;
abstract class MyClassBindings { (1)
@MyInterceptorBinding
abstract String doSomething();
}
@ApplicationScoped
class MyProducer {
@Produces
MyClass produce(@BindingsSource(MyClassBindings.class) InterceptionProxy<MyClass> proxy) { (2)
return proxy.create(new MyClass());
}
}
1 | 一个镜像 MyClass 结构并包含拦截器绑定的类。 |
2 | @BindingsSource 注解表示 MyClass 的拦截器绑定应从 MyClassBindings 读取。 |
绑定源的概念是 InterceptionFactory.configure()
的构建时友好等效项。
生产者方法拦截和合成 bean 拦截仅适用于实例方法。静态方法的拦截不支持生产者方法和合成 bean。 |
5.22.1. 声明 @BindingsSource
@BindingsSource
注解指定一个类,该类镜像被拦截类的结构。然后从该类读取拦截器绑定,并将其视为好像是在被拦截的类上声明的一样。
具体来说:在绑定源类上声明的类级别的拦截器绑定被视为被拦截类的类级别绑定。在绑定源类上声明的方法级别的拦截器绑定被视为被拦截类中具有相同名称、返回类型、参数类型和 static
标志的方法的方法级别绑定。
通常使绑定源类和方法 abstract
,这样您就不必编写方法体
abstract class MyClassBindings {
@MyInterceptorBinding
abstract String doSomething();
}
由于此类永远不会被实例化,并且其方法永远不会被调用,所以这是可以的,但也可以创建一个非 abstract
类
class MyClassBindings {
@MyInterceptorBinding
String doSomething() {
return null; (1)
}
}
1 | 方法体无关紧要。 |
请注意,对于泛型类,类型变量名称也必须相同。例如,对于以下类
class MyClass<T> {
T doSomething() {
...
}
void doSomethingElse(T param) {
...
}
}
绑定源类也必须使用 T
作为类型变量的名称
abstract class MyClassBindings<T> {
@MyInterceptorBinding
abstract T doSomething();
}
您不需要声明未注解的方法,仅仅因为它们存在于被拦截的类上。如果您想向方法的子集添加方法级别的绑定,您只需要声明应该具有拦截器绑定的方法。如果您只想添加类级别的绑定,则根本不需要声明任何方法。
这些注解可以出现在绑定源类上
-
拦截器绑定:在类和方法上
-
定型:在类上
-
@NoClassInterceptors
:在方法上
绑定源类上存在的任何其他注解都将被忽略。
5.22.2. 合成 Bean
在合成 bean 中使用 InterceptionProxy
类似。
首先,您必须声明您的合成 bean 注入 InterceptionProxy
public void register(RegistrationContext context) {
context.configure(MyClass.class)
.types(MyClass.class)
.injectInterceptionProxy() (1)
.creator(MyClassCreator.class)
.done();
}
1 | 再一次,这意味着在构建时,会生成 MyClass 的子类,该子类执行拦截和转发。 |
其次,您必须从 BeanCreator
中的 SyntheticCreationalContext
获取 InterceptionProxy
并使用它
public MyClass create(SyntheticCreationalContext<MyClass> context) {
InterceptionProxy<MyClass> proxy = context.getInterceptionProxy(); (1)
return proxy.create(new MyClass());
}
1 | 获取 MyClass 的 InterceptionProxy ,如上所述。也可以使用 getInjectedReference() 方法,传递一个 TypeLiteral ,但 getInterceptionProxy() 更容易。 |
还有一个与 @BindingsSource
等效的东西。injectInterceptionProxy()
方法有一个带有参数的重载
public void register(RegistrationContext context) {
context.configure(MyClass.class)
.types(MyClass.class)
.injectInterceptionProxy(MyClassBindings.class) (1)
.creator(MyClassCreator.class)
.done();
}
1 | 参数是绑定源类。 |
5.23. Instance.Handle.close()
行为
根据 CDI 规范,Instance.Handle.close()
方法总是委托给 destroy()
。在 ArC 中,这仅在严格模式中成立。
在默认模式下,只有当 bean 是 @Dependent
时(或者当实例句柄不代表 CDI 上下文对象时),close()
方法才会委托给 destroy()
。当实例句柄代表任何其他作用域的 bean 时,close()
方法不执行任何操作;bean 保持原样,并在其上下文被销毁时销毁。
这是为了使以下代码的行为符合人们的期望
Instance<T> instance = ...;
try (Instance.Handle<T> handle : instance.getHandle()) {
T value = handle.get();
... use value ...
}
@Dependent
bean 会立即销毁,而其他 bean 则根本不会销毁。当 Instance
可能返回多个不同作用域的 bean 时,这一点很重要。
6. 反应式编程的陷阱
CDI 是一个纯同步框架。它的异步概念非常有限,仅基于线程池和线程卸载。因此,将 CDI 与反应式编程一起使用时,存在许多陷阱。
6.1. 检测何时允许阻塞
可以使用 io.quarkus.runtime.BlockingOperationControl#isBlockingAllowed()
方法来检测当前线程上是否允许阻塞。当不允许时,并且您需要执行阻塞操作,您必须将其卸载到另一个线程。最简单的方法是使用 Vertx.executeBlocking()
方法
import io.quarkus.runtime.BlockingOperationControl;
@ApplicationScoped
public class MyBean {
@Inject
Vertx vertx;
@PostConstruct
void init() {
if (BlockingOperationControl.isBlockingAllowed()) {
somethingThatBlocks();
} else {
vertx.executeBlocking(() -> {
somethingThatBlocks();
return null;
});
}
}
void somethingThatBlocks() {
// use the file system or JDBC, call a REST service, etc.
Thread.sleep(5000);
}
}
6.2. 异步观察者
CDI 异步观察者 (@ObservesAsync
) 不知道反应式编程,也不打算用作反应式管道的一部分。观察者方法应该是同步的,它们只是卸载到线程池。
Event.fireAsync()
方法返回一个 CompletionStage
,该 CompletionStage
在所有观察者都被通知后完成。如果所有观察者都已成功通知,则 CompletionStage
以事件有效负载完成。如果某些观察者抛出了异常,则 CompletionStage
以 CompletionException
异常完成。
观察者的返回类型无关紧要。观察者的返回值被忽略。
您可以声明一个返回类型为 CompletionStage<>
或 Uni<>
的观察者方法,但是返回类型和实际返回值都不会影响 Event.fireAsync()
的结果。此外,如果观察者声明了 Uni<>
的返回类型,则返回的 Uni
将不会被订阅,因此很可能观察者逻辑的某些部分甚至不会执行。
因此,建议观察者方法(同步和异步)始终声明为 void
。
8. 开发模式
在开发模式下,会自动注册两个特殊端点,以 JSON 格式提供一些基本的调试信息
-
HTTP GET
/q/arc
- 返回摘要;bean 的数量、配置属性等。 -
HTTP GET
/q/arc/beans
- 返回所有 bean 的列表-
您可以使用查询参数来过滤输出
-
scope
- 包括作用域以给定值结尾的 bean,例如https://:8080/q/arc/beans?scope=ApplicationScoped
-
beanClass
- 包括 bean 类以给定值开头的 bean,例如https://:8080/q/arc/beans?beanClass=org.acme.Foo
-
kind
- 包括指定类型的 bean (CLASS
、PRODUCER_FIELD
、PRODUCER_METHOD
、INTERCEPTOR
或SYNTHETIC
),例如https://:8080/q/arc/beans?kind=PRODUCER_METHOD
-
-
-
HTTP GET
/q/arc/removed-beans
- 返回在构建期间删除的未使用 bean 的列表 -
HTTP GET
/q/arc/observers
- 返回所有观察者方法的列表
这些端点仅在开发模式下可用,即当您通过 mvn quarkus:dev (或 ./gradlew quarkusDev )运行您的应用程序时。 |
9. 严格模式
默认情况下,ArC 不执行 CDI 规范要求的所有验证。它还在许多方面提高了 CDI 的可用性,其中一些方面直接违反了规范。
为了通过 CDI Lite TCK,ArC 还具有严格模式。此模式启用了额外的验证并禁用了与规范冲突的某些改进。
要启用严格模式,请使用以下配置
quarkus.arc.strict-compatibility=true
一些其他功能也会影响规范兼容性
为了获得更接近规范的行为,应禁用这些功能。
建议应用程序使用默认的非严格模式,这使得 CDI 使用起来更方便。严格模式的“严格性”(CDI 规范之上的额外验证集和禁用的改进集)可能会随着时间的推移而变化。
10. ArC 配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
||
---|---|---|---|---|
一个未使用的 bean
环境变量: 显示更多 |
字符串 |
|
||
如果设置为 true,则会自动将 环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 true,则将转换不可代理 bean 的字节码。这确保可以正确创建代理/子类。如果值设置为 false,则会在构建时抛出一个异常,表明无法创建子类/代理。启用此设置后,Quarkus 会执行以下转换
环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 true,则注入点的私有字段的字节码将转换为包私有。这确保可以完全无反射地执行字段注入。如果该值设置为 false,则使用反射回退来执行注入。 环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 true(默认值),则如果既不是观察者也不是生产者,而是使用拦截器绑定来注解私有方法,则构建失败。一个例子是在 bean 的私有方法上使用 环境变量: 显示更多 |
布尔值 |
|
||
应用程序的选定替代方案列表。 元素值可以是
每个元素值都用于匹配替代 bean 类、替代定型注解类型或声明替代生产者的 bean 类。如果任何值匹配,则 环境变量: 显示更多 |
字符串列表 |
|||
如果设置为 true,则自动将 环境变量: 显示更多 |
布尔值 |
|
||
应从发现中排除的类型列表。 元素值可以是
如果任何元素值与发现的类型匹配,则该类型将从发现中排除,即不会从此类型创建 bean 和观察者方法。 环境变量: 显示更多 |
字符串列表 |
|||
无论是否直接使用,都应视为不可删除的类型列表。这是一个与使用 元素值可以是
如果任何元素值匹配已发现的 bean,则此类 bean 被视为不可删除。 环境变量: 显示更多 |
字符串列表 |
|||
类型 |
默认 |
|||
工件的 Maven groupId。 环境变量: 显示更多 |
字符串 |
必需 |
||
工件的 Maven artifactId(可选)。 环境变量: 显示更多 |
字符串 |
|||
工件的 Maven classifier(可选)。 环境变量: 显示更多 |
字符串 |
|||
如果设置为 true,则容器尝试检测运行时程序化查找期间的“未使用的已删除 bean”误报。您可以禁用此功能以在生产环境中运行应用程序时节省一些内存。 环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 true,则容器尝试检测注解的错误用法,并最终使构建失败,以防止 Quarkus 应用程序出现意外行为。 一个典型的例子是 环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 引入严格模式主要是为了允许通过 CDI Lite TCK。建议应用程序使用默认的非严格模式,这使得 CDI 使用起来更方便。严格模式的“严格性”(CDI 规范之上的额外验证集和禁用的改进集)可能会随着时间的推移而变化。 请注意, 环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 true,则容器在开发模式下监控业务方法调用和触发的事件。
环境变量: 显示更多 |
布尔值 |
|
||
如果设置为 环境变量: 显示更多 |
|
|
||
如果设置为 true,则在测试期间禁用在应用程序 bean 类上声明的 环境变量: 显示更多 |
布尔值 |
|
||
将不会检查拆分包问题的包列表。 包字符串表示可以是
环境变量: 显示更多 |
字符串列表 |
|||
如果设置为 true 并且存在 SmallRye 上下文传播扩展,则 CDI 上下文将通过 MicroProfile 上下文传播 API 进行传播。具体来说,会注册一个 环境变量: 显示更多 |
布尔值 |
|