JavaRush /Java 博客 /Random-ZH /简单介绍一下依赖注入或“CDI 还有什么?”
Viacheslav
第 3 级

简单介绍一下依赖注入或“CDI 还有什么?”

已在 Random-ZH 群组中发布
现在构建最流行框架的基础是依赖注入。我建议看看 CDI 规范对此有何规定,我们拥有哪些基本功能以及如何使用它们。
简要介绍依赖注入或

介绍

我想将这篇简短的评论专门用于 CDI 这样的事情。这是什么?CDI 代表上下文和依赖注入。这是一个描述依赖注入和上下文的 Java EE 规范。有关信息,您可以查看网站http://cdi-spec.org。由于 CDI 是一个规范(对它如何工作的描述,一组接口),我们还需要一个实现来使用它。其中一种实现是 Weld - http://weld.cdi-spec.org/ 为了管理依赖项并创建项目,我们将使用 Maven - https://maven.apache.org 所以,我们已经安装了 Maven,现在我们会在实践中理解它,以免理解抽象。为此,我们将使用 Maven 创建一个项目。让我们打开命令行(在Windows中,可以使用Win+R打开“运行”窗口并执行cmd)并要求Maven为我们做一切。为此,Maven 有一个称为原型的概念:Maven Archetype
简要介绍依赖注入或
之后,在“选择数字或应用过滤器”和“选择 org.apache.maven.archetypes:maven-archetype-quickstart 版本”问题上,只需按 Enter 键即可。接下来,输入项目标识符,即所谓的 GAV(请参阅命名约定指南)。
简要介绍依赖注入或
成功创建项目后,我们将看到“BUILD SUCCESS”字样。现在我们可以在我们最喜欢的 IDE 中打开我们的项目。

将 CDI 添加到项目

在介绍中,我们看到 CDI 有一个有趣的网站 - http://www.cdi-spec.org/。有一个下载部分,其中包含一个表,其中包含我们需要的数据:
简要介绍依赖注入或
这里我们可以看到Maven是如何描述我们在项目中使用CDI API这一事实的。API是应用程序编程接口,即某种编程接口。我们使用界面而不用担心界面背后的内容和工作方式。API是一个jar档案,我们将在我们的项目中开始使用它,也就是说,我们的项目开始依赖这个jar。因此,我们项目的 CDI API 是一个依赖项。在 Maven 中,项目在 POM.xml 文件(POM - 项目对象模型)中进行描述。依赖项在依赖项块中描述,我们需要向其中添加一个新条目:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
您可能已经注意到,我们没有指定所提供的值的范围。为什么会有这样的差异呢?这个范围意味着有人将为我们提供依赖。当一个应用程序运行在Java EE服务器上时,意味着该服务器将为该应用程序提供所有必需的JEE技术。为了使本次审查简单起见,我们将在 Java SE 环境中工作,因此没有人会为我们提供这种依赖项。您可以在此处阅读有关依赖范围的更多信息:“依赖范围”。好的,我们现在可以使用接口了。但我们还需要执行。我们记得,我们将使用焊接。有趣的是,到处都给出了不同的依赖关系。但我们会遵循文档。因此,让我们阅读“ 18.4.5.设置类路径”并按照其说明进行操作:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
重要的是 Weld 的第三行版本支持 CDI 2.0。因此,我们可以信赖这个版本的API。现在我们准备编写代码了。
简要介绍依赖注入或

初始化 CDI 容器

CDI是一种机制。必须有人控制这个机制。正如我们上面已经读到的,这样的管理器是一个容器。因此,我们需要创建它;它本身不会出现在SE环境中。让我们将以下内容添加到 main 方法中:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
我们手动创建 CDI 容器是因为...... 我们在 SE 环境中工作。在典型的战斗项目中,代码运行在服务器上,服务器为代码提供各种技术。因此,如果服务器提供了CDI,这意味着服务器已经有一个CDI容器,我们不需要添加任何东西。但出于本教程的目的,我们将采用 SE 环境。另外,容器就在这里,清晰易懂。为什么我们需要容器?里面的容器包含豆子(CDI豆)。
简要介绍依赖注入或

CDI 豆

所以,豆子。什么是 CDI 垃圾箱?这是一个遵循一些规则的 Java 类。这些规则在规范的“ 2.2. bean 是什么类型? ”一章中进行了描述。让我们将 CDI bean 添加到与 App 类相同的包中:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
现在我们可以从我们的方法中调用这个 bean main
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
正如您所看到的,我们没有使用 new 关键字创建 bean。我们询问 CDI 容器:“CDI 容器。我真的需要 Logger 类的实例,请给我。” 这种方法称为“ Dependency Lookup ”,即寻找依赖关系。现在让我们创建一个新类:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
返回日期的文本表示形式的原始类。现在让我们将日期输出添加到消息中:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
出现了一个有趣的@Inject注释。如 cdi 焊接文档的“ 4.1. 注入点”一章中所述,我们使用此注释定义注入点。在俄语中,这可以理解为“实施点”。CDI 容器使用它们在实例化 bean 时注入依赖项。正如您所看到的,我们没有为 dateSource 字段分配任何值。原因是 CDI 容器允许内部 CDI bean(仅是它自己实例化的那些 bean,即它管理的 bean)使用“依赖注入”。这是控制反转的另一种方式,在这种方式中,依赖关系由其他人控制,而不是由我们显式创建对象。依赖注入可以通过方法、构造函数或字段来完成。更多细节请参见CDI规范章节“ 5.5.依赖注入”。确定需要实现什么的过程称为类型安全解析,这就是我们需要讨论的内容。
简要介绍依赖注入或

名称解析或类型安全解析

通常,接口用作要实现的对象类型,CDI 容器本身决定选择哪个实现。这很有用,有很多原因,我们将讨论这些原因。所以我们有一个记录器接口:
public interface Logger {
    void print(String message);
}
他说,如果我们有一些记录器,我们可以向它发送一条消息,它就会完成它的任务 - 记录。在这种情况下,如何以及在哪里并不重要。现在让我们为记录器创建一个实现:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
正如您所看到的,这是一个写入 System.out 的记录器。精彩的。现在,我们的 main 方法将像以前一样工作。 Logger logger = container.select(Logger.class).get(); 记录器仍会收到该行。美妙之处在于我们只需要知道接口,CDI 容器已经为我们考虑了实现。假设我们有第二个实现,应该将日志发送到远程存储的某个位置:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
如果我们现在运行代码而不进行任何更改,我们将收到错误,因为 CDI 容器看到该接口的两个实现,但无法在它们之间进行选择: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger 要做什么?有几种可用的变体。最简单的一个是CDI bean 的@Vetoed注释,以便 CDI 容器不会将此类视为 CDI bean。但还有一种更有趣的方法。可以使用Weld CDI 文档的“ 4.7. 替代品@Alternative”一章中描述的注释将 CDI bean 标记为“替代品” 。这是什么意思?这意味着除非我们明确地说使用它,否则它不会被选择。这是 bean 的替代版本。让我们将 NetworkLogger bean 标记为@Alternative,我们可以看到代码再次被执行并被 SystemOutLogger 使用。为了启用替代方案,我们必须有一个beans.xml文件。可能会出现这样的问题:“ beans.xml,我把你放在哪里? ” 因此,让我们正确放置文件:
简要介绍依赖注入或
一旦我们有了这个文件,包含我们代码的工件将被称为“显式 bean 存档”。现在我们有 2 个独立的配置:软件和 xml。问题是他们将加载相同的数据。例如,DataSource bean 定义将被加载 2 次,我们的程序在执行时会崩溃,因为 CDI 容器会将它们视为 2 个独立的 bean(尽管实际上它们是同一个类,CDI 容器了解了两次)。为了避免这种情况,有两种选择:
  • 删除该行initializer.addPackages(App.class.getPackage())并添加 xml 文件的替代指示:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • bean-discovery-mode将值为“ none ”的属性添加到 beans 根元素并以编程方式指定替代方案:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
因此,使用 CDI 替代方案,容器可以确定选择哪个 bean。有趣的是,如果 CDI 容器知道同一接口的多个替代方案,那么我们可以通过使用注释指示优先级来告诉它@Priority(自 CDI 1.1 起)。
简要介绍依赖注入或

预选赛

另外,值得讨论一下限定符这样的事情。限定符由 bean 上方的注释指示,并细化对 bean 的搜索。现在还有更多细节。有趣的是,任何 CDI bean 在任何情况下都至少有一个限定符 - @Any。如果我们没有在 bean 上方指定任何限定符,但 CDI 容器本身会@Any向限定符添加另一个限定符 - @Default。如果我们指定任何内容(例如,显式指定@Any),则不会自动添加@Default 限定符。但预选赛的美妙之处在于您可以创建自己的预选赛。限定符与注释几乎没有什么不同,因为 本质上,这只是一个以特殊方式编写的注释。例如,您可以输入 Enum 作为协议类型:
public enum ProtocolType {
    HTTP, HTTPS
}
接下来我们可以创建一个将这种类型考虑在内的限定符:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
值得注意的是,标记为的字段@Nonbinding并不影响限定符的确定。现在您需要指定限定符。它显示在 bean 类型上方(以便 CDI 知道如何定义它)和注入点上方(使用 @Inject 注释,以便您了解在该位置查找哪个 bean 进行注入)。例如,我们可以添加一些带有限定符的类。为简单起见,在本文中,我们将在 NetworkLogger 中执行这些操作:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
然后,当我们执行 Inject 时,我们将指定一个限定符来影响将使用哪个类:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
太棒了,不是吗?)看起来很美,但不清楚为什么。现在想象一下:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
这样我们就可以覆盖获取值,以便可以动态计算它。例如,它可以从某些设置中获取。然后我们甚至可以即时更改实现,而无需重新编译或重新启动程序/服务器。它变得更有趣,不是吗?)
简要介绍依赖注入或

制片人

CDI 的另一个有用的功能是生产者。这些是特殊方法(它们用特殊注释标记),当某些 bean 请求依赖项注入时调用。更多详细信息在文档的“ 2.2.3. 生产者方法”部分中进行了描述。最简单的例子:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
现在,当注入 Integer 类型的字段时,将调用此方法并从中获取值。这里我们应该立即明白,当我们看到关键字new时,我们必须立即明白这不是一个CDI bean。也就是说,Random 类的实例不会仅仅因为它派生自控制 CDI 容器的对象(在本例中为生产者)而成为 CDI bean。
简要介绍依赖注入或

拦截器

拦截器是“干扰”工作的拦截器。在 CDI 中,这一点做得非常清楚。让我们看看如何使用解释器(或拦截器)进行日志记录。首先,我们需要描述与拦截器的绑定。与许多事情一样,这是使用注释完成的:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
这里最重要的是,这是一个拦截器(@InterceptorBinding)的绑定,它将被extends(@InterceptorBinding)继承。现在让我们编写拦截器本身:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
您可以在规范的示例中阅读有关如何编写拦截器的更多信息:“ 1.3.6.拦截器示例”。好吧,我们所要做的就是打开inerceptor。为此,请在正在执行的方法上方指定绑定注释:
@ConsoleLog
public void print(String message) {
现在还有另一个非常重要的细节。拦截器默认被禁用,必须以与替代方案相同的方式启用。例如,在beans.xml文件中:
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
正如您所看到的,这非常简单。
简要介绍依赖注入或

事件与观察员

CDI 还提供了事件和观察者的模型。这里一切并不像拦截器那么明显。因此,本例中的事件绝对可以是任何类;描述不需要任何特殊内容。例如:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
现在应该有人等待该事件:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
这里最主要的是指定@Observes注解,它表明这不仅仅是一个方法,而是一个应该作为观察LogEvent类型事件的结果而被调用的方法。好吧,现在我们需要有人来观看:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
我们有一个方法可以告诉容器事件类型 LogEvent 的 Event 事件已发生。现在剩下的就是使用观察者了。例如,在 NetworkLogger 中,我们可以添加观察者的注入:
@Inject
private LogObserver observer;
在 print 方法中,我们可以通知观察者我们有一个新事件:
public void print(String message) {
	observer.observe(new LogEvent());
重要的是要知道事件可以在一个或多个线程中处理。对于异步处理,请使用方法.fireAsync(而不是 .fire)和注释@ObservesAsync(而不是 @Observes)。例如,如果所有事件都在不同的线程中执行,那么如果 1 个线程抛出异常,则其他线程将能够为其他事件执行其工作。像往常一样,您可以在规范中的“ 10. 事件”一章中阅读有关 CDI 中事件的更多信息。
简要介绍依赖注入或

装饰器

正如我们在上面看到的,各种设计模式都收集在 CDI 翼下。这是另一个 - 装饰器。这是一件非常有趣的事情。我们来看看这个类:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
通过将其声明为装饰器,我们可以说,当使用任何 Logger 实现时,都会使用这个“附加组件”,它知道真正的实现,该实现存储在 delegate 字段中(因为它是用注释标记的@Delegate)。装饰器只能与 CDI bean 关联,它本身既不是拦截器也不是装饰器。规范中还可以看到一个例子:“ 1.3.7.装饰器示例”。装饰器和拦截器一样,必须打开。例如,在beans.xml中:
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
有关更多详细信息,请参阅焊接参考:“第 10 章装饰器”。

生命周期

Bean 有自己的生命周期。它看起来像这样:
简要介绍依赖注入或
从图中可以看出,我们有所谓的生命周期回调。这些注释将告诉 CDI 容器在 bean 生命周期的某个阶段调用某些方法。例如:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
当容器实例化 CDI bean 时将调用此方法。当 bean 不再需要时被销毁时,@PreDestroy 也会发生同样的情况。首字母缩略词 CDI 包含字母 C(Context)并非毫无意义。CDI 中的 Bean 是上下文相关的,这意味着它们的生命周期取决于它们在 CDI 容器中存在的上下文。为了更好地理解这一点,您应该阅读规范部分“ 7.上下文实例的生命周期”。还值得了解的是,容器本身具有生命周期,您可以在“容器生命周期事件”中阅读有关该生命周期的信息。
简要介绍依赖注入或

全部的

上面我们看到了名为 CDI 的冰山一角。CDI 是 JEE 规范的一部分,用于 JavaEE 环境。使用Spring的不是使用CDI,而是DI,即这些是略有不同的规范。但了解并理解上述内容后,您可以轻松改变主意。考虑到 Spring 支持来自 CDI 世界的注释(相同的 Inject)。附加材料: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION