Quarkus - 上下文依赖注入(CDI)

Quarkus DI 方案是基于 Java 上下文依赖注入 2.0 标准. 但是它不是一个符合 TCK 的完整 CDI 实现。 只实现了 CDI 一部分功能 —— 可以查看支持及限制列表 支持功能清单 and 限制清单

现有的 CDI 代码大多能正常工作,但是由于 Quarkus 架构及目标会有一些小差异。

1. Bean 发现

CDI 中 Bean 发现是一个复杂的过程,涉及传统部署结构和基础模块体系结构的可访问性要求。 但是,Quarkus使用的是简化的 Bean 发现。 There is only single bean archive with the bean discovery mode annotated and no visibility boundaries.

Bean 归档是根据以下内容合成的:

  • application 类,

  • 包含 beans.xml 的依赖项 (将忽略内容),

  • 包含 Jandex 索引的依赖项 - META-INF/jandex.idx,

  • application.propertiesquarkus.index-dependency 用到的依赖,

  • Quarkus integration code.

不会发现没有注解 bean defining annotation 的 Bean 类。 此行为由CDI定义。 但是,包含 producer 方法、字段和 observer 方法的类,即使类未注解也会被发现(此行为与CDI中定义的行为不同)。 实际上,注解了 @Dependent 的类表示可以发现。

Quarkus 扩展可以声明其他发现规则。例如,即使声明类没有注解 @Scheduled 业务方法也会注册。

1.1. 如何生成 Jandex 索引

具有 Jandex 索引的依赖项会自动扫描 beans 。 要生成索引,只需将以下内容添加到 pom.xml

<build>
  <plugins>
    <plugin>
      <groupId>org.jboss.jandex</groupId>
      <artifactId>jandex-maven-plugin</artifactId>
      <version>1.0.7</version>
      <executions>
        <execution>
          <id>make-index</id>
          <goals>
            <goal>jandex</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

如果您无法修改依赖项,则可以添加到 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)

例如,以下设置确保对 org.acme:acme-api 依赖项进行索引:

quarkus.index-dependency.acme.group-id=org.acme (1)
quarkus.index-dependency.acme.artifact-id=acme-api (2)
1 值是名为 acme 依赖项的 group id.
2 值是名为 acme 依赖项的 artifact id.

2. 原生执行文件与私有成员

Quarkus 使用 GraalVM 构建原生可执行文件。 GraalVM 的限制之一是 反射 的使用。 支持反射操作,但必须为所有相关成员进行显式注册以进行反射。 这些注册会导致更大的原生可执行文件。

如果 Quarkus DI需要访问私有成员,则必须使用反射。 因此,Quarkus 鼓励用户不要在 bean 中使用私有成员。 这涉及注入字段,构造函数和初始化程序,观察者方法,生产者方法和字段,处理程序和拦截器方法。(This involves injection fields, constructors and initializers, observer methods, producer methods and fields, disposers and interceptor methods. )

如何避免使用私有成员? 您可以使用 package-private 修饰符:

@ApplicationScoped
public class CounterBean {

    @Inject
    CounterService counterService; (1)

    void onMessage(@Observes Event msg) { (2)
    }
}
1 package-private 注入字段.
2 package-private 监听方法.

或通过构造函数注入:

@ApplicationScoped
public class CounterBean {

    private CounterService service;

    CounterBean(CounterService service) { (1)
      this.service = service;
    }
}
1 package-private 构造函数注入. 这种情况 @Inject 是可选的.

3. 支持的功能

  • Programming model

    • Managed beans implemented by a Java class

      • @PostConstruct and @PreDestroy lifecycle callbacks

    • Producer methods and fields, disposers

    • Qualifiers

    • Alternatives

    • Stereotypes

  • Dependency injection and lookup

    • Field, constructor and initializer/setter injection

    • Type-safe resolution

    • Programmatic lookup via javax.enterprise.inject.Instance

    • Client proxies

    • Injection point metadata

  • Scopes and contexts

    • @Dependent, @ApplicationScoped, @Singleton, @RequestScoped and @SessionScoped

    • Custom scopes and contexts

  • Interceptors

    • Business method interceptors: @AroundInvoke

    • Interceptors for lifecycle event callbacks: @PostConstruct, @PreDestroy, @AroundConstruct

  • Events and observer methods, including asynchronous events and transactional observer methods

4. 限制

  • 不支持 @ConversationScoped

  • 不支持 Decorators

  • 不支持 Portable Extensions are not supported

  • BeanManager - only the following methods are implemented: getBeans(), createCreationalContext(), getReference(), getInjectableReference() , resolve(), getContext(), fireEvent(), getEvent() and createInstance()

  • 不支持 Specialization

  • beans.xml 声明会被忽略

  • 不支持 Passivation and passivating scopes are not supported

  • Interceptor methods on superclasses are not implemented yet

5. 非标准功能

5.1. 尽早初始化 Beans

5.1.1. 默认是延迟创建

默认,在需要时可以延迟创建 CDI bean。 “需要”的含义取决于bean的范围。

  • normal scoped bean (@ApplicationScoped, @RequestScoped, etc.) 调用被注入实例的方法时创建 (contextual reference per the specification).

    换言之,注入一个普通范围的 bean 不会创建,因为注入的是 CDI bean 的 客户代理

  • 伪范围(pseudo-scope) bean (@Dependent and @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. Startup 事件

但是,如果您真的需要早创建 bean,则可以:

  • 声明一个StartupEvent observer - 这种情况下,bean的范围无关紧要:

    @ApplicationScoped
    class CoolService {
      void startup(@Observes StartupEvent event) { (1)
      }
    }
    1 CoolService 在启动过程中创建来为观察者方法调用提供服务。
  • Use the bean in an observer of the StartupEvent - normal scoped beans must be used as described in 默认是延迟创建:

    @Dependent
    class MyBeanStarter {
    
      void startup(@Observes StartupEvent event, AmazingService amazing, CoolService cool) { (1)
        cool.toString(); (2)
      }
    }
    1 `AmazingService`在注入时创建。
    2 CoolService 是普通范围 bean ,所以我们必须调用注入的代理方法来强制实例化。
应用启动停止 指南中说明了,相对 @Initialized(ApplicationScoped.class) Quarkus 鼓励优先用 @Observes StartupEvent

5.2. 请求上下文生命周期

The request context is also active:

  • during notification of a synchronous observer method.

The request context is destroyed:

  • after the observer notification completes for an event, if it was not already active when the notification started.

An event with qualifier @Initialized(RequestScoped.class) is fired when the request context is initialized for an observer notification. Moreover, the events with qualifiers @BeforeDestroyed(RequestScoped.class) and @Destroyed(RequestScoped.class) are fired when the request context is destroyed.

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;
  }
}

There is no need to declare dummy constructors for normal scoped bean in Quarkus - they are generated automatically. Also if there’s only one constructor there is no need for @Inject.

不需要为 Quarkus 中的普通作用域bean 声明空构造函数 - 它们是自动生成的。 另外,如果只有一个构造函数,则不需要 @Inject

@ApplicationScoped
public class MyCoolService {

  private SimpleProcessor processor;

  MyCoolService(SimpleProcessor processor) {
    this.processor = processor;
  }
}
如果Bean类扩展了未声明无参构造函数的类,则我们不会自动生成无参构造函数。

5.5. 删除未使用的 Beans

默认情况下,容器会尝试在构建期间删除所有未使用的bean。可以通过设置 quarkus.arc.remove-unused-beansnonefalse 来禁止。

未使用的bean:

  • is not a built-in bean or an interceptor,

  • is not eligible for injection to any injection point,

  • is not excluded by any extension,

  • does not have a name,

  • does not declare an observer,

  • does not declare any producer which is eligible for injection to any injection point,

  • is not directly eligible for injection into any javax.enterprise.inject.Instance or javax.inject.Provider injection point

This optimization applies to all forms of bean declarations: bean class, producer method, producer field.

用户可以通过用注解 io.quarkus.arc.Unremovable 来指示容器不要删除任何特定的bean(即使它们满足上述所有规则)。 该注释可以放在类型,生产者方法和生产者字段上。

此外,扩展可以通过产生 UnremovableBeanBuildItem 来消除可能的误删。

Finally, Quarkus provides a middle ground for the bean removal optimization where application beans are never removed whether or not they are unused, while the optimization proceeds normally for non application classes. To use this mode, set quarkus.arc.remove-unused-beans to fwk or framework.

When using the dev mode (running ./mvnw clean compile quarkus:dev), you can see more information about which beans are being removed by enabling additional logging via the following line in your application.properties.

使用开发模式(运行 ./mvnw clean compile quarkus:dev )时,可以通过 application.properties 中下列配置启用详细日志了解要删除哪些 bean 。

quarkus.log.category."io.quarkus.arc.processor".level=DEBUG

5.6. 默认 Beans

Quarkus adds a capability that CDI currently does not support which is to conditionally declare a bean if no other bean with equal types and qualifiers was declared by any available means (bean class, producer, synthetic bean, …​) This is done using the @io.quarkus.arc.DefaultBean annotation and is best explained with an example.

假设有一个 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
    }
}

The idea is that the extension auto-configures things for the user, eliminating a lot of boilerplate - we can just @Inject a Tracer wherever it is needed. Now imagine that in our application we would like to utilize the configured Tracer, but we need to customize it a little, for example by providing a custom Reporter. The only thing that would be needed in our application would be something like the following:

@Dependent
public class CustomTracerConfiguration {

    @Produces
    public Reporter reporter(){
        // create a custom Reporter
    }
}

@DefaultBean allows extensions (or any other code for that matter) to provide defaults while backing off if beans of that type are supplied in any way Quarkus supports.

6. Build Time Extension Points

6.1. Portable Extensions

Quarkus incorporates build-time optimizations in order to provide instant startup and low memory footprint. The downside of this approach is that CDI Portable Extensions cannot be supported. Nevertheless, most of the functionality can be achieved using Quarkus extensions.

6.2. Additional Bean Defining Annotations

As described in Bean 发现 bean classes that don’t have a bean defining annotation are not discovered. However, BeanDefiningAnnotationBuildItem can be used to extend the set of default bean defining annotations (@Dependent, @Singleton, @ApplicationScoped, @RequestScoped and @Stereotype annotations):

@BuildStep
BeanDefiningAnnotationBuildItem additionalBeanDefiningAnnotation() {
    return new BeanDefiningAnnotationBuildItem(DotName.createSimple("javax.ws.rs.Path")));
}
Bean registrations that are result of a BeanDefiningAnnotationBuildItem are unremovable by default. See also 删除未使用的 Beans.

6.3. Resource Annotations

ResourceAnnotationBuildItem is used to specify resource annotations that make it possible to resolve non-CDI injection points, such as Java EE resources.

An integrator must also provide a corresponding io.quarkus.arc.ResourceReferenceProvider implementation.
@BuildStep
void setupResourceInjection(BuildProducer<ResourceAnnotationBuildItem> resourceAnnotations, BuildProducer<GeneratedResourceBuildItem> resources) {
    resources.produce(new GeneratedResourceBuildItem("META-INF/services/io.quarkus.arc.ResourceReferenceProvider",
        JPAResourceReferenceProvider.class.getName().getBytes()));
    resourceAnnotations.produce(new ResourceAnnotationBuildItem(DotName.createSimple(PersistenceContext.class.getName())));
}

6.4. Additional Beans

AdditionalBeanBuildItem is used to specify additional bean classes to be analyzed during discovery. Additional bean classes are transparently added to the application index processed by the container.

@BuildStep
List<AdditionalBeanBuildItem> additionalBeans() {
     return Arrays.asList(
          new AdditionalBeanBuildItem(SmallRyeHealthReporter.class),
          new AdditionalBeanBuildItem(HealthServlet.class));
}
A bean registration that is a result of an AdditionalBeanBuildItem is removable by default. See also 删除未使用的 Beans.

6.5. Synthetic Beans

Sometimes it is very useful to register a synthetic bean, i.e. a bean that doesn’t need to have a corresponding java class. In CDI, this could be achieved using AfterBeanDiscovery.addBean() methods. In Quarkus, we produce a BeanRegistrarBuildItem and leverage the io.quarkus.arc.processor.BeanConfigurator API to build a synthetic bean definition.

@BuildStep
BeanRegistrarBuildItem syntheticBean() {
     return new BeanRegistrarBuildItem(new BeanRegistrar() {

            @Override
            public void register(RegistrationContext registrationContext) {
                 registrationContext.configure(String.class).types(String.class).qualifiers(new MyQualifierLiteral()).creator(mc -> mc.returnValue(mc.load("foo"))).done();
            }
        }));
}
The output of a BeanConfigurator is recorded as bytecode. Therefore there are some limitations in how a synthetic bean instance is created. See also BeanConfigurator.creator() methods.
You can easily filter all class-based beans via the convenient BeanStream returned from the RegistrationContext.beans() method.

If an extension needs to produce other build items during the "bean registration" phase it should use the BeanRegistrationPhaseBuildItem instead. The reason is that injected objects are only valid during a @BuildStep method invocation.

@BuildStep
void syntheticBean(BeanRegistrationPhaseBuildItem beanRegistrationPhase,
            BuildProducer<MyBuildItem> myBuildItem,
            BuildProducer<BeanConfiguratorBuildItem> beanConfigurators) {
   beanConfigurators.produce(new BeanConfiguratorBuildItem(beanRegistrationPhase.getContext().configure(String.class).types(String.class).qualifiers(new MyQualifierLiteral()).creator(mc -> mc.returnValue(mc.load("foo")))));
   myBuildItem.produce(new MyBuildItem());
}
See BeanRegistrationPhaseBuildItem javadoc for more information.

6.6. Annotation Transformations

A very common task is to override the annotations found on the bean classes. For example you might want to add an interceptor binding to a specific bean class. Here is how to do it - use the AnnotationsTransformerBuildItem:

@BuildStep
AnnotationsTransformerBuildItem transform() {
    return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

        public boolean appliesTo(org.jboss.jandex.AnnotationTarget.Kind kind) {
            return kind == org.jboss.jandex.AnnotationTarget.Kind.CLASS;
        }

        public void transform(TransformationContext context) {
            if (contex.getTarget().asClass().name().toString().equals("com.foo.Bar")) {
                context.transform().add(MyInterceptorBinding.class).done();
            }
        }
    });
}

6.7. Additional Interceptor Bindings

In rare cases it might be handy to programmatically register an existing annotation as interceptor binding. This is similar to what pure CDI achieves through BeforeBeanDiscovery#addInterceptorBinding(). Though here we are going to use InterceptorBindingRegistrarBuildItem to get it done. Note that you can register multiple annotations in one go:

@BuildStep
InterceptorBindingRegistrarBuildItem addInterceptorBindings() {
    InterceptorBindingRegistrarBuildItem additionalBindingsRegistrar = new InterceptorBindingRegistrarBuildItem(new InterceptorBindingRegistrar() {
        @Override
        public Collection<DotName> registerAdditionalBindings() {
            Collection<DotName> result = new HashSet<>();
            result.add(DotName.createSimple(MyAnnotation.class.getName()));
            result.add(DotName.createSimple(MyOtherAnnotation.class.getName()));
            return result;
        }
    });
    return additionalBindingsRegistrar;
}

6.8. Injection Point Transformation

Every now and then it is handy to be able to change qualifiers of an injection point programmatically. You can do just that with InjectionPointTransformerBuildItem. The following sample shows how to apply transformation to injection points with type Foo that contain qualifier MyQualifier:

@BuildStep
InjectionPointTransformerBuildItem transformer() {
    return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() {

        public boolean appliesTo(Type requiredType) {
            return requiredType.name().equals(DotName.createSimple(Foo.class.getName()));
        }

        public void transform(TransformationContext context) {
            if (context.getQualifiers().stream()
                    .anyMatch(a -> a.name().equals(DotName.createSimple(MyQualifier.class.getName())))) {
                context.transform()
                        .removeAll()
                        .add(DotName.createSimple(MyOtherQualifier.class.getName()))
                        .done();
            }
        }
    });
}

6.9. Observer Transformation

Any observer method definition can be vetoed or transformed using an ObserverTransformerBuildItem. The attributes that can be transformed include:

@BuildStep
ObserverTransformerBuildItem transformer() {
    return new ObserverTransformerBuildItem(new ObserverTransformer() {

        public boolean appliesTo(Type observedType, Set<AnnotationInstance> qualifiers) {
            return observedType.name.equals(DotName.createSimple(MyEvent.class.getName()));
        }

        public void transform(TransformationContext context) {
            // Veto all observers of MyEvent
            context.veto();
        }
    });
}

6.10. Bean Deployment Validation

Once the bean deployment is ready an extension can perform additional validations and inspect the found beans, observers and injection points. Register a BeanDeploymentValidatorBuildItem:

@BuildStep
BeanDeploymentValidatorBuildItem beanDeploymentValidator() {
    return new BeanDeploymentValidatorBuildItem(new BeanDeploymentValidator() {
         public void validate(ValidationContext validationContext) {
             for (InjectionPointInfo injectionPoint : validationContext.get(Key.INJECTION_POINTS)) {
                 System.out.println("Injection point: " + injectionPoint);
             }
         }
    });
}
You can easily filter all registered beans via the convenient BeanStream returned from the ValidationContext.beans() method.

If an extension needs to produce other build items during the "validation" phase it should use the ValidationPhaseBuildItem instead. The reason is that injected objects are only valid during a @BuildStep method invocation.

@BuildStep
void validate(ValidationPhaseBuildItem validationPhase,
            BuildProducer<MyBuildItem> myBuildItem,
            BuildProducer<ValidationErrorBuildItem> errors) {
   if (someCondition) {
     errors.produce(new ValidationErrorBuildItem(new IllegalStateException()));
     myBuildItem.produce(new MyBuildItem());
   }
}
See ValidationPhaseBuildItem javadoc for more information.

6.11. Custom Contexts

An extension can register a custom InjectableContext implementation by means of a ContextRegistrarBuildItem:

@BuildStep
ContextRegistrarBuildItem customContext() {
    return new ContextRegistrarBuildItem(new ContextRegistrar() {
         public void register(RegistrationContext registrationContext) {
            registrationContext.configure(CustomScoped.class).normal().contextClass(MyCustomContext.class).done();
         }
    });
}

If an extension needs to produce other build items during the "context registration" phase it should use the ContextRegistrationPhaseBuildItem instead. The reason is that injected objects are only valid during a @BuildStep method invocation.

@BuildStep
void addContext(ContextRegistrationPhaseBuildItem contextRegistrationPhase,
            BuildProducer<MyBuildItem> myBuildItem,
            BuildProducer<ContextConfiguratorBuildItem> contexts) {
   contexts.produce(new ContextConfiguratorBuildItem(contextRegistrationPhase.getContext().configure(CustomScoped.class).normal().contextClass(MyCustomContext.class)));
   myBuildItem.produce(new MyBuildItem());
}
See ContextRegistrationPhaseBuildItem javadoc for more information.

6.12. Available Build Time Metadata

Any of the above extensions that operates with BuildExtension.BuildContext can leverage certain build time metadata that are generated during build. The built-in keys located in io.quarkus.arc.processor.BuildExtension.Key are:

  • ANNOTATION_STORE

    • Contains an AnnotationStore that keeps information about all AnnotationTarget annotations after application of annotation transformers

  • INJECTION_POINTS

    • Collection<InjectionPointInfo> containing all injection points

  • BEANS

    • Collection<BeanInfo> containing all beans

  • REMOVED_BEANS

  • OBSERVERS

    • Collection<ObserverInfo> containing all observers

  • SCOPES

    • Collection<ScopeInfo> containing all scopes, including custom ones

  • QUALIFIERS

    • Map<DotName, ClassInfo> containing all qualifiers

  • INTERCEPTOR_BINDINGS

    • Map<DotName, ClassInfo> containing all interceptor bindings

  • STEREOTYPES

    • Map<DotName, ClassInfo> containing all stereotypes

To get hold of these, simply query the extension context object for given key. Note that these metadata are made available as build proceeds which means that extensions can only leverage metadata that were build before they are invoked. If your extension attempts to retrieve metadata that wasn’t yet produced, null will be returned. Here is a summary of which extensions can access which metadata:

  • AnnotationsTransformer

    • Shouldn’t rely on any metadata as this is one of the first CDI extensions invoked

  • ContextRegistrar

    • Has access to ANNOTATION_STORE

  • InjectionPointsTransformer

    • Has access to ANNOTATION_STORE, QUALIFIERS, INTERCEPTOR_BINDINGS, STEREOTYPES

  • ObserverTransformer

    • Has access to ANNOTATION_STORE, QUALIFIERS, INTERCEPTOR_BINDINGS, STEREOTYPES

  • BeanRegistrar

    • Has access to ANNOTATION_STORE, QUALIFIERS, INTERCEPTOR_BINDINGS, STEREOTYPES, BEANS

  • BeanDeploymentValidator

    • Has access to all build metadata

7. ArC Configuration Reference

Configuration property fixed at build time - All other configuration properties are overridable at runtime

Configuration property

Type

Default

  • If set to all (or true) the container will attempt to remove all unused beans.

  • If set to none (or false) no beans will ever be removed even if they are unused (according to the criteria set out below)

  • If set to fwk, then all unused beans will be removed, except the unused beans whose classes are declared in the application code

    An unused bean:
    - is not a built-in bean or interceptor,
    - is not eligible for injection to any injection point,
    - is not excluded by any extension,
    - does not have a name,
    - does not declare an observer,
    - does not declare any producer which is eligible for injection to any injection point,
    - is not directly eligible for injection into any `javax.enterprise.inject.Instance` injection point

string

all

If set to true @Inject is automatically added to all non-static fields that are annotated with one of the annotations defined by AutoInjectAnnotationBuildItem.

boolean

true

If set to true, Arc will transform the bytecode of beans containing methods that need to be proxyable but have been declared as final. The transformation is simply a matter of removing final. This ensures that a proxy can be created properly. If the value is set to false, then an exception is thrown at build time indicating that a proxy could not be created because a method was final.

boolean

true

The default naming strategy for ConfigProperties.NamingStrategy. The allowed values are determined by that enum

from-config, verbatim, kebab-case

kebab-case

quarkus.pro 是基于 quarkus.io 的非官方中文翻译站 ,最后更新 2020/04 。
沪ICP备19006215号-8
QQ交流群:1055930959
微信群: