前言
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
}
}