七叶笔记 » golang编程 » 云原生-Quarkus上下文依赖注入介绍

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

前言

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

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类型。在上面的示例中, Translator bean具有两种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 。

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

因此, @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
   }

}  

相关文章