云原生-Quarkus上下文依赖注入介绍

927 阅读9分钟

前言

CDI(Contexts and Dependency Injection),即上下文依賴注入,是J2EE6推出的一個標标准规范,用于对上下文依赖注入的标准规范化,思想应该是來来源于Spring的IOC。

云原生-Quarkus上下文依赖注入介绍

1、什么是Bean?

bean是一个_容器管理(container-managed)的_对象,它支持一些基本服务,例如依赖项的注入,生命周期回调和拦截器。

2、容器管理(container-managed)介绍

简而言之,不必直接控制对象实例的生命周期。相反,可以通过声明性方式(例如注解,配置等)影响生命周期。容器是应用程序运行所在的_环境_。它创建并销毁bean的实例,将实例与指定的上下文关联,然后将其注入其他bean。

开发人员可以专注于业务逻辑,而不必找出“何处和如何”来获得具有所有依赖关系的完全初始化的组件。

(IoC)编程原理:依赖注入是IoC的一种实现技术。

3、bean的示例

bean有多种类型,其中最常见的基于类型的bean

`import javax.inject.Inject;`
`import javax.enterprise.context.ApplicationScoped;`
`import org.eclipse.microprofile.metrics.annotation.Counted;`
`@ApplicationScoped  //1一个范围注解。它告诉容器与Bean实例关联的上下文。在这种特殊情况下,将为应用程序创建一个bean实例,并由所有其他其他bean注入使用Translator。`
`public class Translator {`
 `@Inject //一个字段注入点。它告诉Translator依赖于Dictionarybean的容器。如果没有匹配的bean,构建将失败。`
 `Dictionary dictionary;` 
 `@Counted  //3这是一个拦截器绑定注解。在这种情况下,注解来自MicroProfile Metrics。相关拦截器拦截调用并更新相关指标。`
 `String translate(String sentence) {`
 `// ...`
 `}`
`}`

4、依赖解析如何工作?

在CDI中,将bean匹配到注入点的过程是类型安全的。每个bean声明一组bean类型。在上面的示例中,Translatorbean具有两种bean类型:Translator和java.lang.Object。随后,如果Bean具有与_所需类型_匹配并具有所有_必需限定符_的Bean类型,则该Bean可分配给注入点。

5、多个bean声明相同的类型?

有一个简单的规则:必须将一个bean确切地分配给一个注入点,否则构建将失败。如果没有可分配的,则构建将失败
UnsatisfiedResolutionException。如果可分配多个,则构建将失败
AmbiguousResolutionException。这非常有用,因为只要容器找不到任何注入点的明确依赖关系,应用程序就会快速失败。

可以使用编程查找通过
javax.enterprise.inject.Instance来解决运行时的歧义,甚至遍历实现给定类型的所有bean:

`public class Translator {`
 `@Inject`
 `Instance<Dictionary> dictionaries;  //1即使有多个实现该Dictionary类型的bean,该注入点也不会导致模棱两可的依赖关系。`
 `String translate(String sentence) {`
 `for (Dictionary dict : dictionaries) { //2javax.enterprise.inject.Instance延伸Iterable。`
 `// ...`
 `}`
 `}`
`}`

6、可以使用setter和构造函数注入

实际上,在CDI中,“ setter注入”已被更强大的初始化方法所取代。初始化程序可以接受多个参数,而不必遵循JavaBean命名约定。

初始化和构造函数注入示例

`@ApplicationScoped`
`public class Translator {`
 `private final TranslatorHelper helper;`
 `Translator(TranslatorHelper helper) {  //1这是一个构造函数注入。实际上,此代码在常规CDI实现中不起作用,在常规CDI实现中,具有正常作用域的bean必须始终声明一个no-args构造函数,并且该Bean构造函数必须使用注释@Inject。但是,在Quarkus中,我们检测到没有no-args构造函数,并将其直接“添加”到字节码中。@Inject如果只存在一个构造函数,也不必添加。`
 `this.helper = helper;`
 `}`
 `@Inject //2初始化方法必须用注释@Inject。`
 `void setDeps(Dictionary dic, LocalizationService locService) { //3初始化程序可以接受多个参数-每个参数都是一个注入点。`
 `/ ...`
 `}`
`}`

7、关于qualifiers

@Qualifier,一个注解,是帮助容器区分实现相同的bean的注释类型。如果一个bean具有所有必需的限定符,则可以将其分配给注入点。如果您在注入点未声明任何限定符,那么就使用的是@Default

限定符类型是定义为@Retention(RUNTIME)的Java注解,并使用@ javax.inject.Qualifier元注解进行注解。

示例

`@Qualifier`
`@Retention(RUNTIME)`
`@Target({METHOD, FIELD, PARAMETER, TYPE})`
`public @interface Superior {}`

通过用Qualifier注解Bean类或生产者方法或字段来声明Bean的限定符:

带有自定义限定词的Bean示例

`@Superior  //1@Superior是一个限定符注释。`
`@ApplicationScoped`
`public class SuperiorTranslator extends Translator {`
 `String translate(String sentence) {`
 `// ...`
 `}`
`}`

该bean可以分配给@Inject @Superior Translator,@Inject @Superior SuperiorTranslator但不能分配给@Inject Translator。原因是在类型安全解析期间@Inject Translator会自动将其转换为@Inject @Default Translator。并且由于我们SuperiorTranslator没有声明@Default只有原始Translatorbean是可分配的。

8、什么是Bean的作用域

bean的scope决定了它的实例的生命周期,即:什么时候什么位置实例被创建和销毁。(每一个bean都有一个准确的范围)

9、Quarkus中Bean有哪些作用域

可以使用规范中提到的所有内置作用域(除外)
javax.enterprise.context.ConversationScoped。

注解

描述

@javax.enterprise.context.ApplicationScoped

单个bean实例用于该应用程序,并在所有注入点之间共享。实例是惰性创建的,即一旦在客户端proxy上调用了方法。

@javax.inject.Singleton

就像@ApplicationScoped除了不使用任何客户端代理一样。当注入解析为@Singleton bean的注入点时,将创建该实例。

@javax.enterprise.context.RequestScoped

Bean实例与当前_请求_(通常是HTTP请求)相关联。

@javax.enterprise.context.Dependent

这是一个伪作用域。这些实例不共享,并且每个注入点都会生成一个新的依赖Bean实例。从属bean的生命周期与注入它的bean绑定-将与注入它的bean一起创建和销毁它。

@javax.enterprise.context.SessionScoped

该范围由一个javax.servlet.http.HttpSession对象支持。仅在使用quarkus-undertow扩展名的情况下可用。

  • Quarkus扩展可以提供其他自定义范围。例如,quarkus-narayana-jta提供javax.transaction.TransactionScoped。

10、@ApplicationScoped和@Singleton外观很相似。应该选择哪一个?

取决于:一个@Singleton bean有没有客户端代理,因此是一个实例_急切地创建_时,bean被注入。相比之下,@ApplicationScoped bean的实例是_延迟创建的_,即,第一次在注入的实例上调用方法时。

此外,客户端代理仅委托方法调用,因此,永远不应@ApplicationScoped直接读取/写入注入的Bean的字段。可以@Singleton安全地读取/写入注入的字段。

@Singleton 应该具有更好的性能,因为没有间接作用(没有从上下文委托给当前实例的代理)。

另一方面,不能@Singleton使用QuarkusMock模拟bean 。

@ApplicationScopedBean也可以在运行时销毁并重新创建。现有的注入点可以正常工作,因为注入的代理委托给当前实例。

因此,@ApplicationScoped除非有充分的理由使用,否则我们建议默认情况下坚持使用@Singleton。

11、客户端代理(client proxies)概念

客户端代理基本上是一个将所有方法调用委托给目标Bean实例的对象。这是一个实现
io.quarkus.arc.ClientProxy并扩展bean类的容器构造。它实现
io.quarkus.arc.ClientProxy并继承了bean类。(客户端代理仅委托方法调用。因此,不能读取或写入普通作用域bean的字段,否则将使用非上下文或陈旧的数据。)

生成的客户端代理示例

`@ApplicationScoped`
`class Translator {`
 `String translate(String sentence) {`
 `// ...`
 `}`
`}`
`// The client proxy class is generated and looks like...`
`class Translator_ClientProxy extends Translator { //1Translator_ClientProxy总是注入该实例,而不是直接引用该bean的上下文实例Translator。`
 `String translate(String sentence) {`
 `// Find the correct translator instance...`
 `Translator translator = getTranslatorInstanceFromTheApplicationContext();`
 `// And delegate the method invocation...`
 `return translator.translate(sentence);`
 `}`
`}`

客户代理可以:

  • 延迟实例化-在代理上调用方法后即创建实例。
  • 能够将作用域“更窄”的bean注入作用域“更宽”的bean(例如:可以将@RequestScoped bean注入@ApplicationScoped bean)。
  • 依赖关系图中的循环依赖关系。具有循环依赖关系通常表明应考虑重新设计,但有时这不可避免。
  • 可以手动销毁bean,直接注入的引用将导致过时的bean实例。

12、bean的类型

  • Class bean
  • Producer methods
  • Producer fields
  • Synthetics(复合) beans //一般由扩展提供

如果您需要对bean的实例化进行其他控制,则生产者方法和字段很有用。在集成第三方库时,无法控制类源并且可能不能添加其他注解,也比较有用。

生产者示例

`@ApplicationScoped`
`public class Producers {`
 `@Produces //1容器分析字段注释以构建Bean元数据。该类型用于构建bean类型集。在这种情况下,它将是double和java.lang.Object。没有声明作用域注释,因此默认为@Dependent。`
 `double pi = Math.PI; //2创建bean实例时,容器将读取此字段。`
 `@Produces //3容器分析方法注释以构建Bean元数据。在返回类型被用来建立一套豆种。在这种情况下,这将是List<String>,Collection<String>,Iterable<String>和java.lang.Object。没有声明作用域注释,因此默认为@Dependent。`
 `List<String> names() {`
 `List<String> names = new ArrayList<>();`
 `names.add("Andy");`
 `names.add("Adalbert");`
 `names.add("Joachim");`
 `return names; //创建bean实例时,容器将调用此方法`
 `}`
`}`
`@ApplicationScoped`
`public class Consumer {`
 `@Inject`
 `double pi;`
 `@Inject`
 `List<String> names;`
 `// ...`
`}`

关于生产者的更多信息。您可以声明限定符,将依赖项注入到生产者方法参数中,等等。

13、注解-生命周期回调

Bean类可以声明生命周期@PostConstruct和@PreDestroy回调:

生命周期回调示例

`import javax.annotation.PostConstruct;`
`import javax.annotation.PreDestroy;`
`@ApplicationScoped`
`public class Translator {`
 `@PostConstruct   //1在bean实例加入服务之前被调用,在此处执行初始化是安全的。`
 `void init() {`
 `// ...`
 `}`
 `@PreDestroy //2在bean实例销毁前被调用,在这里执行一些清理任务是安全的`
 `void destroy() {`
 `// ...`
 `}`
`}`

最好在回调函数中保持逻辑“无副作用”,即应避免在回调函数中调用其他bean。

14、注解-拦截器

拦截器用于将跨领域的关注点与业务逻辑分开。有一个单独的规范-Java Interceptors-定义了基本的编程模型和语义。

简单拦截器示例

`import javax.interceptor.Interceptor;`
`import javax.annotation.Priority;`
`@Logged  //1这是一个拦截器绑定批注,用于将我们的拦截器绑定到bean。只需使用注释一个bean类@Logged。`
`@Priority(2020) //2Priority启用拦截器并影响拦截器的顺序。具有较小优先级值的拦截器首先被称为。`
`@Interceptor //3标记拦截器组件。`
`public class LoggingInterceptor {`
 `@Inject //4拦截器实例可能是依赖项注入的目标。`
 `Logger logger;`
 `@AroundInvoke //5AroundInvoke 表示插入业务方法的方法。`
 `Object logInvocation(InvocationContext context) {`
 `// ...log before`
 `Object ret = context.proceed(); #6进入拦截器链中的下一个拦截器,或调用被拦截的业务方法。`
 `// ...log after`
 `return ret;`
 `}`
`}`

拦截器的实例是它们拦截的Bean实例的相关对象,即,为每个拦截的Bean创建一个新的拦截器实例。

15、事件与观察者(Events and Observers)

Bean还可以产生和消耗事件,以完全分离的方式进行交互。任何Java对象都可以充当事件有效负载。可选的限定词充当主题选择器。

简单事件示例

`class TaskCompleted {`
 `// ...`
`}`
`@ApplicationScoped`
`class ComplicatedService {`
 `@Inject`
 `Event<TaskCompleted> event;  //1javax.enterprise.event.Event 用于引发事件。`
 `void doSomething() {`
 `// ...`
 `event.fire(new TaskCompleted()); //2同步触发事件。`
 `}`
`}`
`@ApplicationScoped`
`class Logger {`
 `void onTaskCompleted(@Observes TaskCompleted task) { //3TaskCompleted触发事件时会通知此方法。`
 `// ...log the task`
 `}`
`}`