Java CDI(上下文和依赖注入)简介

An Introduction to CDI (Contexts and Dependency Injection) in Java

1.概述

CDI(上下文和依赖注入)是Java EE 6和更高版本中包含的标准依赖注入框架。

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

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

2. DYDI(自己做依赖注入)

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

这种方法被普遍称为DYDI(自己做依赖注入)。

使用DYDI,我们通过简单的旧工厂/构建器将所需的依赖项传递到客户端类中,从而使应用程序代码与对象创建隔离开来。

这是基本的DYDI实现的样子:

1
2
3
4
public interface TextService {
    String doSomethingWithText(String text);
    String doSomethingElseWithText(String text);    
}

1
public class SpecializedTextService implements TextService { ... }

1
2
3
4
5
public class TextClass {
    private TextService textService;
   
    // constructor
}

1
2
3
4
5
6
public class TextClassFactory {
     
    public TextClass getTextClass() {
        return new TextClass(new SpecializedTextService();
    }    
}

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

如果我们的示例应用程序的大小和复杂性增加,实现一个更大的互连对象网络,我们最终将被大量的对象图工厂所污染。

仅用于创建对象图就需要大量样板代码。 这不是一个完全可扩展的解决方案。

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

3.一个简单的例子

CDI将DI变成了一个简单的过程,归结为只用几个简单的注释修饰服务类,并在客户端类中定义了相应的注入点。

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

3.1。" beans.xml"文件

首先,我们必须将" beans.xml"文件放置在" src / main / resources / META-INF /"文件夹中。 即使此文件根本不包含任何特定的DI指令,它也是启动和运行CDI所必需的:

1
2
3
4
5
<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。 服务类别

接下来,让我们创建用于对GIF,JPG和PNG文件执行上述操作的文件的服务类:

1
2
3
4
5
6
public interface ImageFileEditor {
    String openFile(String fileName);
    String editFile(String fileName);
    String writeFile(String fileName);
    String saveFile(String fileName);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
    }
}

1
2
3
4
public class JpgFileEditor implements ImageFileEditor {
    // JPG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}

1
2
3
4
public class PngFileEditor implements ImageFileEditor {
    // PNG-specific implementations for openFile() / editFile() / writeFile() / saveFile()
    ...
}

3.3。 客户类

最后,让我们实现一个在构造函数中采用ImageFileEditor实现的客户端类,并使用@Inject批注定义一个注入点:

1
2
3
4
5
6
7
8
9
public class ImageFileProcessor {
   
    private ImageFileEditor imageFileEditor;
   
    @Inject
    public ImageFileProcessor(ImageFileEditor imageFileEditor) {
        this.imageFileEditor = imageFileEditor;
    }
}

简而言之,@ Inject注释是CDI的主要功能。 它允许我们在客户端类中定义注入点。

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

此外,还可以通过在字段(字段注入)和设置器(setter注入)中使用@Inject注释来注入服务。 稍后我们将介绍这些选项。

3.4。 用Weld构建ImageFileProcessor对象图

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

为此,首先,我们应该获得该类的实例。

由于我们不会依赖任何Java EE应用程序服务器来使用CDI,因此我们将使用Weld(Java SE中的CDI参考实现)进行此操作:

1
2
3
4
5
6
7
8
9
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();
}

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

不出所料,如果我们运行该应用程序,则CDI将通过抛出DeploymentException大声抱怨:

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

因为CDI不知道要注入ImageFileProcessor构造函数的ImageFileEditor实现,所以我们遇到了此异常。

在CDI的术语中,这被称为歧义注入异常。

3.5。 @Default和@Alternative注释

解决这种歧义很容易。 默认情况下,CDI使用@Default注释对接口的所有实现进行注释。

因此,我们应该明确告诉它应该将哪种实现注入客户端类:

1
2
@Alternative
public class GifFileEditor implements ImageFileEditor { ... }

1
2
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }

1
public class PngFileEditor implements ImageFileEditor { ... }

在这种情况下,我们用@Alternative注释对GifFileEditor和JpgFileEditor进行注释,因此CDI现在知道PngFileEditor(默认情况下用@Default注释进行注释)是我们要注入的实现。

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

1
Opening PNG file file1.png

此外,使用@Default注释对PngFileEditor进行注释,并将其他实现保留为替代方案将产生与上述相同的结果。

简而言之,这显示了我们如何通过简单地在服务类中切换@Alternative批注就可以非常容易地交换实现的运行时注入。

4.场注入

CDI开箱即用地支持现场注射和二传手注射。

这是执行字段注入的方法(带有@Default和@Alternative批注的合格服务的规则保持不变):

1
2
@Inject
private final ImageFileEditor imageFileEditor;

5.套注射

同样,以下是进行setter注入的方法:

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

6. @Named注释

到目前为止,我们已经学习了如何在客户端类中定义注入点以及如何使用@Inject,@Default和@Alternative批注来注入服务,这些批注涵盖了大多数用例。

尽管如此,CDI还允许我们使用@Named注释执行服务注入。

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

1
2
3
4
5
6
7
8
@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

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

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

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

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

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

1
2
3
4
5
@Inject
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

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

7. @Produces批注

有时,在注入服务以处理其他依赖项之前,服务需要对某些配置进行完全初始化。

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

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

为了了解@Produces批注的工作方式,让我们重构ImageFileProcessor类,以便它可以在构造函数中使用附加的TimeLogger服务。

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

1
2
3
4
5
6
7
8
@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }
   
public String openFile(String fileName) {
    return imageFileEditor.openFile(fileName) +" at:" + timeLogger.getTime();
}
   
// additional image file methods

在这种情况下,TimeLogger类将接受两个附加服务,即SimpleDateFormat和Calendar:

1
2
3
4
5
6
7
8
9
10
11
public class TimeLogger {
   
    private SimpleDateFormat dateFormat;
    private Calendar calendar;
   
    // constructors
   
    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

我们如何告诉CDI在何处获取完全初始化的TimeLogger对象-

我们只是创建一个TimeLogger工厂类,并使用@Produces注释来注释其工厂方法:

1
2
3
4
5
6
7
public class TimeLoggerFactory {
   
    @Produces
    public TimeLogger getTimeLogger() {
        return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
    }
}

每当我们获得ImageFileProcessor实例时,CDI都会扫描TimeLoggerFactory类,然后调用getTimeLogger()方法(使用@Produces批注进行批注),最后注入Time Logger服务。

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

1
Opening PNG file file1.png at: 17:46

8.自定义限定词

CDI支持使用自定义限定符来限定依赖关系并解决歧义注入点。

自定义限定词是一项非常强大的功能。 它们不仅将语义名称绑定到服务,而且还绑定注入元数据。 元数据,例如RetentionPolicy和法律注释目标(ElementType)。

让我们看看如何在应用程序中使用自定义限定符:

1
2
3
4
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface GifFileEditorQualifier {}

1
2
3
4
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface JpgFileEditorQualifier {}

1
2
3
4
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PngFileEditorQualifier {}

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

1
2
@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }

1
2
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }

1
2
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

最后,让我们在ImageFileProcessor类中重构注入点:

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

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

自定义限定符为将名称和注释元数据绑定到实现提供了一种简洁的语义方法。

此外,自定义限定符使我们可以定义更多限制性的类型安全注入点(优于@Default和@Alternative批注的功能)。

如果只有一个子类型在类型层次结构中合格,则CDI将仅注入该子类型,而不注入基本类型。

9.结论

毫无疑问,CDI使依赖注入变得轻而易举,额外注释的成本为获得有组织的依赖注入付出了很少的努力。

有时DYDI仍然比CDI更具优势。 就像在开发只包含简单对象图的相当简单的应用程序时一样。

与往常一样,本文中显示的所有代码示例均可在GitHub上获得。