An Introduction to CDI (Contexts and Dependency Injection) in Java – Java中的CDI(上下文和依赖注入)简介

最后修改: 2018年 6月 25日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

CDI (Contexts and Dependency Injection) is a standard dependency injection framework included in Java EE 6 and higher.

CDI(Contexts and Dependency Injection)是Java EE 6及以上版本中包含的标准依赖注入框架。

It allows us to manage the lifecycle of stateful components via domain-specific lifecycle contexts and inject components (services) into client objects in a type-safe way.

它允许我们通过特定领域的生命周期上下文来管理有状态组件的生命周期,并以类型安全的方式将组件(服务)注入客户端对象。

In this tutorial, we’ll take an in-depth look at CDI’s most relevant features and implement different approaches for injecting dependencies in client classes.

在本教程中,我们将深入了解CDI最相关的功能,并实现在客户端类中注入依赖关系的不同方法。

2. DYDI (Do-it-Yourself Dependency Injection)

2.DYDI(Do-it-Yourself Dependency Injection)

In a nutshell, it’s possible to implement DI without resorting to any framework at all.

简而言之,不借助任何框架就可以实现DI。

This approach is popularly known as DYDI (Do-it-Yourself Dependency Injection).

这种方法被普遍称为DYDI(Do-it-Yourself Dependency Injection)。

With DYDI, we keep application code isolated from object creation by passing the required dependencies into the client classes through plain old factories/builders.

有了DYDI,我们通过普通的工厂/构建器将所需的依赖关系传递到客户端类中,使应用程序代码与对象创建隔离开来。

Here’s how a basic DYDI implementation might look like:

下面是一个基本的DYDI实现的样子。

public interface TextService {
    String doSomethingWithText(String text);
    String doSomethingElseWithText(String text);    
}
public class SpecializedTextService implements TextService { ... }
public class TextClass {
    private TextService textService;
    
    // constructor
}
public class TextClassFactory {
      
    public TextClass getTextClass() {
        return new TextClass(new SpecializedTextService(); 
    }    
}

Of course, DYDI is suitable for some relatively simple use cases.

当然,DYDI适用于一些相对简单的用例。

If our sample application grew in size and complexity, implementing a larger network of interconnected objects, we would end up polluting it with tons of object graph factories.

如果我们的示例应用程序的规模和复杂性增加,实现了一个更大的相互连接的对象网络,我们最终会用大量的对象图工厂来污染它。

This would require a lot of boilerplate code just for creating object graphs. This is not a fully-scalable solution.

这将需要大量的模板代码,只是为了创建对象图。这不是一个完全可扩展的解决方案。

Can we do DI any better? Of course, we can. Here’s exactly where CDI comes into the picture.

我们能把DI做得更好吗?当然,我们可以。这正是CDI发挥作用的地方。

3. A Simple Example

3.一个简单的例子

CDI turns DI into a no-brainer process, boiled down to just decorating the service classes with a few simple annotations, and defining the corresponding injection points in the client classes.

CDI将DI变成了一个不费吹灰之力的过程,只需用一些简单的注解来装饰服务类,并在客户类中定义相应的注入点。

To showcase how CDI implements DI at the most basic level, let’s suppose that we want to develop a simple image file editing application. Capable of opening, editing, writing, saving an image file and so on.

为了展示CDI是如何在最基本的水平上实现DI的,让我们假设我们想开发一个简单的图像文件编辑程序。能够打开、编辑、写入、保存一个图像文件等等。

3.1. The “beans.xml” File

3.1.“beans.xml”文件

First, we must place a “beans.xml” file in the “src/main/resources/META-INF/” folder. Even if this file doesn’t contain any specific DI directives at all, it’s required for getting CDI up and running:

首先,我们必须在“src/main/resources/META-INF/”文件夹中放置一个“beans.xml”文件。即使这个文件根本不包含任何特定的DI指令,它也是启动和运行CDI的必要条件

<beans xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

3.2. The Service Classes

3.2.服务类别

Next, let’s create the service classes that perform the file mentioned above operations on GIF, JPG and PNG files:

接下来,让我们创建服务类,对GIF、JPG和PNG文件进行上述的文件操作。

public interface ImageFileEditor {
    String openFile(String fileName);
    String editFile(String fileName);
    String writeFile(String fileName);
    String saveFile(String fileName);
}
public class GifFileEditor implements ImageFileEditor {
    
    @Override
    public String openFile(String fileName) {
        return "Opening GIF file " + fileName;
    }
    
    @Override
    public String editFile(String fileName) {
      return "Editing GIF file " + fileName;
    }
    
    @Override
    public String writeFile(String fileName) {
        return "Writing GIF file " + fileName;
    }

    @Override
    public String saveFile(String fileName) {
        return "Saving GIF file " + fileName;
    }
}
public class JpgFileEditor implements ImageFileEditor {
    // JPG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}
public class PngFileEditor implements ImageFileEditor {
    // PNG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}

3.3. The Client Class

3.3.客户端类

Finally, let’s implement a client class that takes an ImageFileEditor implementation in the constructor, and let’s define an injection point with the @Inject annotation:

最后,让我们实现一个客户端类,在构造函数中接受一个ImageFileEditor实现,让我们用@Inject注解定义一个注入点。

public class ImageFileProcessor {
    
    private ImageFileEditor imageFileEditor;
    
    @Inject
    public ImageFileProcessor(ImageFileEditor imageFileEditor) {
        this.imageFileEditor = imageFileEditor;
    }
}

Simply put, the @Inject annotation is CDI’s actual workhorse. It allows us to define injection points in the client classes.

简单地说,@Inject注解是CDI的实际工作主体。它允许我们在客户端类中定义注入点。

In this case, @Inject instructs CDI to inject an ImageFileEditor implementation in the constructor.

在这种情况下,@Inject指示CDI在构造函数中注入一个ImageFileEditor实现。

Furthermore, it’s also possible to inject a service by using the @Inject annotation in fields (field injection) and setters (setter injection). We’ll look at these options later.

此外,也可以通过在字段(field injection)和设置器(setter injection)中使用@Inject注解来注入一个服务。我们将在后面看这些选项。

3.4. Building the ImageFileProcessor Object Graph With Weld

3.4.使用Weld构建ImageFileProcessor对象图

Of course, we need to make sure that CDI will inject the right ImageFileEditor implementation into the ImageFileProcessor class constructor.

当然,我们需要确保CDI将正确的ImageFileEditor实现注入到ImageFileProcessor类构造函数中。

To do so, first, we should get an instance of the class.

要做到这一点,首先,我们应该得到一个类的实例。

As we won’t rely on any Java EE application server for using CDI, we’ll do this with Weld, the CDI reference implementation in Java SE:

由于我们不会依赖任何 Java EE 应用服务器来使用 CDI,因此我们将使用 Java SE 中的 CDI 参考实现来实现。

public static void main(String[] args) {
    Weld weld = new Weld();
    WeldContainer container = weld.initialize();
    ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get();
 
    System.out.println(imageFileProcessor.openFile("file1.png"));
 
    container.shutdown();
}

Here, we’re creating a WeldContainer object, then getting an ImageFileProcessor object, and finally calling its openFile() method.

在这里,我们要创建一个WeldContainer对象,然后获得一个ImageFileProcessor对象,最后调用其openFile()方法。

As expected, if we run the application, CDI will complain loudly by throwing a DeploymentException:

正如预期的那样,如果我们运行应用程序,CDI将大声抱怨,抛出一个DeploymentException:

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...

We’re getting this exception because CDI doesn’t know what ImageFileEditor implementation to inject into the ImageFileProcessor constructor.

我们得到了这个异常,因为CDI不知道该将什么ImageFileEditor实现注入到ImageFileProcessor构造器中。

In CDI’s terminology, this is known as an ambiguous injection exception.

在CDI的术语中,这被称为模糊的注入异常

3.5. The @Default and @Alternative Annotations

3.5.@Default@Alternative注解

Solving this ambiguity is easy. CDI, by default, annotates all the implementations of an interface with the @Default annotation.

解决这种含糊不清的问题很容易。CDI默认用@Default注解来注解一个接口的所有实现。

So, we should explicitly tell it which implementation should be injected into the client class:

因此,我们应该明确地告诉它哪个实现应该被注入到客户类中。

@Alternative
public class GifFileEditor implements ImageFileEditor { ... }
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }
public class PngFileEditor implements ImageFileEditor { ... }

In this case, we’ve annotated GifFileEditor and JpgFileEditor with the @Alternative annotation, so CDI now knows that PngFileEditor (annotated by default with the @Default annotation) is the implementation that we want to inject.

在这种情况下,我们用 @Alternative 注解来注释 GifFileEditorJpgFileEditor,所以 CDI 现在知道 PngFileEditor(默认用 @Default 注解来注释)是我们要注入的实现。

If we rerun the application, this time it’ll be executed as expected:

如果我们重新运行该应用程序,这次将按预期执行。

Opening PNG file file1.png

Furthermore, annotating PngFileEditor with the @Default annotation and keeping the other implementations as alternatives will produce the same above result.

此外,用@Default注解来注解PngFileEditor,并保留其他实现作为替代,也会产生上述相同的结果。

This shows, in a nutshell, how we can very easily swap the run-time injection of implementations by simply switching the @Alternative annotations in the service classes.

简而言之,这表明我们可以通过简单地交换服务类中的@Alternative注解来非常容易地交换实现的运行时注入

4. Field Injection

4.场内注射

CDI supports both field and setter injection out of the box.

CDI开箱即支持字段和设定器的注入。

Here’s how to perform field injection (the rules for qualifying services with the @Default and @Alternative annotations remain the same):

以下是执行字段注入的方法(使用@Default@Alternative注解限定服务的规则仍然相同)。

@Inject
private final ImageFileEditor imageFileEditor;

5. Setter Injection

5.设定器注入

Similarly, here’s how to do setter injection:

类似地,这里是如何做setter注入。

@Inject 
public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. The @Named Annotation

6.@Named 注释

So far, we’ve learned how to define injection points in client classes and inject services with the @Inject, @Default , and @Alternative annotations, which cover most of the use cases.

到目前为止,我们已经学会了如何在客户端类中定义注入点,并通过@Inject@Default@Alternative注解注入服务,这些注解涵盖了大多数的使用情况。

Nevertheless, CDI also allows us to perform service injection with the @Named annotation.

尽管如此,CDI也允许我们用@Named注解来执行服务注入。

This method provides a more semantic way of injecting services, by binding a meaningful name to an implementation:

该方法提供了一种更有语义的注入服务的方式,通过将一个有意义的名称与一个实现绑定:

@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

@Named("JpgFileEditor")
public class JpgFileEditor implements ImageFileEditor { ... }

@Named("PngFileEditor")
public class PngFileEditor implements ImageFileEditor { ... }

Now, we should refactor the injection point in the ImageFileProcessor class to match a named implementation:

现在,我们应该重构ImageFileProcessor类中的注入点,以匹配一个命名的实现。

@Inject 
public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

It’s also possible to perform field and setter injection with named implementations, which looks very similar to using the @Default and @Alternative annotations:

也可以用命名的实现来执行字段和设置器注入,这看起来与使用@Default@Alternative注解非常相似。

@Inject 
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

@Inject 
public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. The @Produces Annotation

7.@Produces注释

Sometimes, a service requires some configuration to be fully-initialized before it gets injected to handle additional dependencies.

有时,一个服务在被注入之前需要一些配置被完全初始化,以处理额外的依赖关系。

CDI provides support for these situations, through the @Produces annotation.

CDI通过@Produces注释为这些情况提供了支持。

@Produces allows us to implement factory classes, whose responsibility is the creation of fully-initialized services.

@Produces允许我们实现工厂类,其职责是创建完全初始化的服务。

To understand how the @Produces annotation works, let’s refactor the ImageFileProcessor class, so it can take an additional TimeLogger service in the constructor.

为了理解@Produces注解是如何工作的,让我们重构ImageFileProcessor类,以便它能在构造函数中接受额外的TimeLogger服务。

The service will be used for logging the time at which a certain image file operation is performed:

该服务将用于记录执行某个图像文件操作的时间。

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... } 
    
public String openFile(String fileName) {
    return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime();
}
    
// additional image file methods

In this case, the TimeLogger class takes two additional services, SimpleDateFormat and Calendar:

在这种情况下,TimeLogger类需要两个额外的服务,SimpleDateFormatCalendar

public class TimeLogger {
    
    private SimpleDateFormat dateFormat;
    private Calendar calendar;
    
    // constructors
    
    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

How do we tell CDI where to look at for getting a fully-initialized TimeLogger object?

我们如何告诉CDI在哪里获得一个完全初始化的TimeLogger对象?

We just create a TimeLogger factory class and annotate its factory method with the @Produces annotation:

我们只需创建一个TimeLogger工厂类,并用@Produces注解来注释其工厂方法。

public class TimeLoggerFactory {
    
    @Produces
    public TimeLogger getTimeLogger() {
        return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
    }
}

Whenever we get an ImageFileProcessor instance, CDI will scan the TimeLoggerFactory class, then call the getTimeLogger() method (as it’s annotated with the @Produces annotation), and finally inject the Time Logger service.

每当我们得到一个ImageFileProcessor实例,CDI将扫描TimeLoggerFactory类,然后调用getTimeLogger()方法(因为它被@Produces注解),最后注入Time Logger服务。

If we run the refactored sample application with Weld, it’ll output the following:

如果我们用Weld运行重构后的示例应用程序,它将输出以下内容。

Opening PNG file file1.png at: 17:46

8. Custom Qualifiers

8.自定义限定词

CDI supports the use of custom qualifiers for qualifying dependencies and solving ambiguous injection points.

CDI支持使用自定义限定器来限定依赖关系和解决模糊的注入点。

Custom qualifiers are a very powerful feature. They not only bind a semantic name to a service, but they bind injection metadata too. Metadata such as the RetentionPolicy and the legal annotation targets (ElementType).

自定义限定词是一项非常强大的功能。它们不仅将语义名称绑定到服务,而且还绑定了注入元数据。元数据,例如保留政策和合法注释目标(元素类型)。

Let’s see how to use custom qualifiers in our application:

让我们看看如何在我们的应用程序中使用自定义限定词。

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface GifFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface JpgFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PngFileEditorQualifier {}

Now, let’s bind the custom qualifiers to the ImageFileEditor implementations:

现在,让我们将自定义的限定符绑定到ImageFileEditor的实现上。

@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

Lastly, let’s refactor the injection point in the ImageFileProcessor class:

最后,让我们重构一下ImageFileProcessor类中的注入点

@Inject
public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

If we run our application once again, it should generate the same output shown above.

如果我们再次运行我们的应用程序,它应该产生与上面所示相同的输出。

Custom qualifiers provide a neat semantic approach for binding names and annotation metadata to implementations.

自定义限定符提供了一种整洁的语义方法,用于将名称和注解元数据绑定到实现中。

In addition, custom qualifiers allow us to define more restrictive type-safe injection points (outperforming the functionality of the @Default and @Alternative annotations).

此外,自定义限定符允许我们定义更具限制性的类型安全注入点(优于@Default和@Alternative注释的功能)

If only a subtype is qualified in a type hierarchy, then CDI will only inject the subtype, not the base type.

如果在一个类型层次结构中只有一个子类型是合格的,那么CDI将只注入子类型,而不是基本类型。

9. Conclusion

9.结论

Unquestionably, CDI makes dependency injection a no-brainer, the cost of the extra annotations is very little effort for the gain of organized dependency injection.

毋庸置疑,CDI使依赖注入变得不费吹灰之力,额外注解的成本对于有组织的依赖注入的收益来说是非常小的。

There are times when DYDI does still have its place over CDI. Like when developing fairly simple applications that only contain simple object graphs.

有些时候,DYDI的确比CDI更有地位。比如在开发相当简单的、只包含简单对象图的应用程序时。

As always, all the code samples shown in this article are available over on GitHub.

一如既往,本文中所展示的所有代码样本都可以在GitHub上找到