Quarkus-依赖注入
Quarkus DI解决方案(也叫ArC)是基于Java 2.0规范的上下文和依赖注入。然而,它并不是一个经过TCK验证的完整的CDI实现。只实现了CDI功能的一个子集。
一、bean发现
bean档案是由以下内容合成的:
1、应用类
2、包含 beans.xml 描述符的依赖项(内容被忽略)。
3、包含Jandex索引的依赖性 - META-INF/jandex.idx。
4、在application.properties中由quarkus.index-dependency引用的依赖性。
5、Quarkus集成代码
没有Bean定义注解的Bean类不会被发现。这种行为是由CDI定义的。但是生产者方法、字段和观察者方法会被发现,即使声明的类没有被注解为bean定义注解(这种行为与CDI中定义的不同)。事实上,声明的Bean类被认为是用@Dependent注解的。
如果你不能修改依赖关系,你仍然可以通过在application.properties中添加quarkus.index-dependency条目来索引它:
quarkus.index-dependency.<name>.group-id=
quarkus.index-dependency.<name>.artifact-id=
quarkus.index-dependency.<name>.classifier=(this one is optional)
可能会发生一些来自第三方库的Bean在Quarkus中不能正常工作。一个典型的例子是一个注入了可移植扩展的Bean。在这种情况下,我们可以从Bean发现中排除类型和依赖关系。quarkus.arc.exclud-types 属性接受一个字符串列表,用来匹配应该被排除的类:
org.acme.Foo 匹配类的完全合格名称
org.acme.* 与包org.acme相匹配的类
org.acme.** 匹配包以org.acme开头的类。
Bar 匹配类的简单名称
比如quarkus.arc.exclude-types=org.acme.Foo,org.acme.*,Bar
排除了org.acme.Foo.Ltd的类型,从org.acme包中排除所有类型。排除所有简单名称为Bar的类型。
quarkus.arc.exclude-dependency.acme.group-id=org.acme
quarkus.arc.exclude-dependency.acme.artifact-id=acme-services
会排除group-id是org.acme,artifact-id是acme-services的依赖。
二、尽量不要使用private成员
Quarkus正在使用GraalVM来构建一个本地可执行文件。GraalVM的限制之一是反射的使用。反射操作是被支持的,但所有相关的成员都必须明确注册为反射。这些注册会导致一个更大的本地可执行文件。如果Quarkus DI需要访问一个私有成员,就必须使用反射。这就是为什么我们鼓励Quarkus用户不要在他们的bean中使用私有成员。这涉及到注入字段、构造器和初始化器、观察者方法、生产者方法和字段、处置者和拦截者方法。使用包修饰符代替private。
三、急切实例化bean
默认情况下,CDI Bean在需要时被懒惰地创建。需要 "到底是什么意思,取决于Bean的范围。当一个方法在一个注入的实例(根据规范的上下文引用)上被调用时,需要一个普通的作用域beanBean(@ApplicationScoped,@RequestScoped,等等)。 换句话说,注入一个正常范围的Bean是不够的,因为注入的是一个客户端代理而不是Bean的上下文实例。 当注入时,一个具有伪范围(@Dependent 和 @Singleton )的bean被创建。即@Dependent和@Singleton作用域的bean是立即创建的。
然而,如果你真的需要急切地实例化一个bean,你可以:
1、声明一个StartupEvent的观察者--在这种情况下,bean的范围并不重要。
@ApplicationScoped
class CoolService {
void startup(@Observes StartupEvent event) {
}
}
2、在StartupEvent的观察者中使用懒加载的Bean。
@Dependent
class MyBeanStarter {
void startup(@Observes StartupEvent event, AmazingService amazing, CoolService cool) {
cool.toString();
}
}
CoolService是懒加载的bean,必须调用toString创建CoolService。
3、使用@io.quarkus.runtime.Startup来注解bean。
@Startup
@ApplicationScoped
public class EagerAppBean {
private final String name;
EagerAppBean(NameGenerator generator) {
this.name = generator.createName();
}
}
对于每个用@Startup注解的Bean,都会生成一个默认优先级的StartupEvent的合成(由编译器创建)观察者。我们鼓励Quarkus用户总是倾向于使用@Observes StartupEvent而不是@Initialized(ApplicationScoped.class)
四、移除未使用的bean
容器在构建过程中,默认会尝试删除所有未使用的Bean、拦截器和装饰器。这种优化有助于尽量减少生成的类的数量,从而节约内存。然而,Quarkus无法检测到通过CDI.current()静态方法进行的程序性查找。因此,删除有可能导致假阳性错误,即一个Bean被删除,尽管它实际上被使用。在这种情况下,你会在日志中注意到一个很大的警告。
可以通过将quarkus.arc.remove-unused-beans设置为none或false来禁用这种优化。Quarkus还提供了一种中间模式,即无论应用Bean是否未使用,都不会被移除,而对非应用类的优化则正常进行。要使用这种模式,请将 quarkus.arc.remove-unused-beans 设置为 fwk 或 framework。
不可移除的bean:
- 被排除在扩展清除之外。
- 有一个通过@Named指定的名称
- 声明了一个观察者方法。
一个未使用的bean:
- 不是不可移除的,并且
- 没有资格被注入到依赖树中的任何注入点,并且
- 没有声明任何有资格被注入依赖树中的任何注入点的生产者,并且
- 没有资格注射到任何 javax.enterprise.injection.Instance 或 javax.injection.Provider 注入点。
可以通过用 @io.quarkus.arc.Unremovable 注释来指示容器不删除他们的任何特定 bean(即使他们满足上面指定的所有规则)。这个注解可以在类、生产者方法或字段上声明。因为这并不总是可能的,所以有一个选项可以通过application.properties实现同样的效果。quarkus.arc.unremovable-types属性接受一个字符串列表,用于根据Bean的名字或包来匹配。此外,扩展可以通过产生一个UnremovableBeanBuildItem来消除假阳性。
五、默认bean
Quarkus增加了一项CDI目前不支持的能力,即如果没有其他具有相同类型和限定符的Bean被任何可用的方法(Bean类、生产者、合成Bean......)声明,则有条件地声明一个Bean。这是用@io.quarkus.arc.DefaultBean注解完成的。
@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
}
}
@Dependent
public class CustomTracerConfiguration {
@Produces
public Reporter reporter(){
// create a custom Reporter
}
}
如果没有自定义的Reporter就用@DefaultBean标识的bean。有了自定义Reporter就用自定义Reporter。
六、启用Quarkus Build Profile的Bean
Quarkus增加了一项CDI目前不支持的功能,即通过@io.quarkus.arc.profile.IfBuildProfile和@io.quarkus.arc.profile.UnlessBuildProfile注解,在Quarkus构建时间配置文件被启用时有条件地启用Bean。当与@io.quarkus.arc.defaultBean结合使用时,这些注解允许为不同的构建配置文件创建不同的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();
}
}
realTracer方法只有在生产时才会执行生成bean。
如果要求Tracer bean在开发模式下也能工作,并且只在测试时默认不做任何事情,那么@UnlessBuildProfile将是理想的选择。代码会看起来像:
@Dependent
public class TracerConfiguration {
@Produces
@UnlessBuildProfile("test")
public Tracer realTracer(Reporter reporter, Configuration configuration) {
return new RealTracer(reporter, configuration);
}
@Produces
@DefaultBean
public Tracer noopTracer() {
return new NoopTracer();
}
}
七、启用Quarkus Build Properties的Bean
Quarkus增加了一项CDI目前不支持的功能,即通过@io.quarkus.arc.properties.IfBuildProperty和@io.quarkus.arc.properties.UnlessBuildProperty注解,在Quarkus构建时间属性有/无特定值时有条件地启用一个Bean。当与@io.quarkus.arc.DefaultBean结合使用时,该注解允许为不同的构建属性创建不同的Bean配置。
@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();
}
}
some.tracer.enabled属性值为true才会启用realTracer标识的bean。
@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();
}
}
如果some.tracer.enabled不为false启用realTracer标识的bean。
八、选定的替代品
在CDI中,可以通过@Priority为应用程序全局选择一个替代的Bean,也可以通过beans.xml描述符为一个Bean档案选择一个替代的Bean。Quarkus有一个简化的bean discovery,beans.xml的内容被忽略了。@javax.annotation.Priority的缺点是它有@Target({ TYPE, PARAMETER }),所以它不能用于生产者方法和字段。这个问题应该在Common Annotations 2.1中得到修正。我们鼓励用户使用@io.quarkus.arc.Priority来代替,直到Quarkus升级到这个版本的jakarta.annotation-api。
然而,也可以使用统一的配置为一个应用程序选择替代物。quarkus.arc.selected-alternatives 属性接受一个字符串列表,用于匹配备选Bean。如果有任何值匹配,那么相关Bean就会使用Integer#MAX_VALUE的优先级。通过@Priority或@AlternativePriority声明的优先级被重写。比如:
quarkus.arc.selected-alternatives=org.acme.Foo,org.acme.*,Bar
九、拦截静态方法
拦截器规范中明确规定,invoke周围的方法不能被声明为静态。然而,这一限制主要是由技术限制所驱动的。而由于Quarkus是一个面向构建时间的堆栈,允许额外的类转换,这些限制不再适用了。我们可以用拦截器绑定来注释一个非私有的静态方法。
class Services {
@Logged
static BigDecimal computePrice(long amount) {
BigDecimal price;
// Perform computations...
return price;
}
}
限制:
1、拦截方法级绑定
2、私有静态方法从不被拦截
3、InvocationContext#getTarget()会返回null;因此,在拦截静态方法时,并非所有现有的拦截器都能正确表现。
十、容器管理的并发性
CDI Bean没有标准的并发控制机制。尽管如此,一个Bean实例可以被共享,并从多个线程中并发访问。在这种情况下,它应该是线程安全的。你可以使用标准的Java结构(volatile、synchronized、ReadWriteLock等)或者让容器控制并发访问。Quarkus为这个拦截器绑定提供了@io.quarkus.arc.Lock和一个内置拦截器。每个与被拦截Bean的上下文实例相关的拦截器实例都持有一个单独的具有非公平排序策略的ReadWriteLock。
@Lock
@ApplicationScoped
class SharedService {
void addAmount(BigDecimal amount) {
// ...changes some internal state of the bean
}
@Lock(value = Lock.Type.READ, time = 1, unit = TimeUnit.SECONDS)
BigDecimal getAmount() {
// ...it is safe to read the value concurrently
}
}
在类上声明的@Lock(映射为@Lock(Lock.Type.WRITE))指示容器为任何业务方法的任何调用锁定bean实例,也就是说,客户端有 "独占访问权",不允许并发调用。@Lock(Lock.Type.READ)覆盖了在类级别指定的值。这意味着任何数量的客户端都可以并发地调用该方法,除非Bean实例被@Lock(Lock.Type.WRITE)锁定。你也可以指定 "等待时间"。如果不可能在给定的时间内获得锁,就会抛出一个LockException。
十一、可重复的拦截器绑定
当把拦截器绑定到一个组件上时,你可以在方法上声明多个@Repeatable注解。不支持在类和定型上声明的可重复拦截器绑定。作为一个例子,假设我们有一个清除缓存的拦截器。相应的拦截器绑定将被称为@CacheInvalidateAll,并且将被声明为@Repeatable。如果我们想同时清除两个缓存,我们将添加@CacheInvalidateAll两次。
@ApplicationScoped
class CachingService {
@CacheInvalidateAll(cacheName = "foo")
@CacheInvalidateAll(cacheName = "bar")
void heavyComputation() {
}
}
十二、缓存计算结果
在某些情况下,通过注入的 javax.enterprise.inject.Instance 和 Instance.get() 以编程方式获取 bean 实例是很实用的。然而,根据规范,get()方法必须识别匹配的Bean并获得上下文引用。因此,每次调用 get() 都会返回一个 @Dependent Bean 的新实例。此外,这个实例是被注入的实例的一个依赖对象。这种行为被定义得很好,但它可能会导致意外的错误和内存泄漏。因此,Quarkus带有io.quarkus.arc.WithCaching注解。用这个注解注入的实例将缓存Instance#get()操作的结果。该结果在第一次调用时就被计算出来,并且在所有后续的调用中都返回相同的值,即使是对于@Dependent Bean。
@Inject
@WithCaching
Instance<Integer> intInstance;
十三、声明性地选择可通过程序化查询获得的bean
想象一下,我们有两个实现org.acme.Service接口的bean。你不能直接注入org.acme.Service,除非你的实现声明了一个CDI限定词。然而,你可以注入Instance
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());
}
}
十四、忽略方法和构造器的类级拦截器绑定
如果一个托管Bean在类级别上声明了拦截器绑定注解,那么相应的@AroundInvoke拦截器将适用于所有业务方法。类似地,相应的 @AroundConstruct 拦截器将适用于bean构造函数。
@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,那么唯一适用于这个方法/构造函数的拦截器就是直接在这个方法/构造函数上声明的拦截器。
十五、异步观察者方法抛出的异常情况
如果一个异常被异步观察者抛出,那么由fireAsync()方法返回的CompletionStage会异常完成,这样事件生产者就可以做出适当的反应。然而,如果事件生产者不关心,那么这个异常就会被无声地忽略。因此,Quarkus默认会记录一条错误信息。也可以实现一个自定义的AsyncObserverExceptionHandler。实现这个接口的Bean应该是@javax.inject.Singleton或@javax.enterprise.context.ApplicationScoped。
@Singleton
public class NoopAsyncObserverExceptionHandler implements AsyncObserverExceptionHandler {
void handle(Throwable throwable, ObserverMethod<?> observerMethod, EventContext<?> eventContext) {
// do nothing
}
}