1. 概述

CDI(Contexts and Dependency Injection)是 Java EE 6 及更高版本中内置的标准依赖注入(DI)框架。

它的核心能力是通过领域特定的上下文来管理有状态组件的生命周期,并以类型安全的方式将服务注入到客户端对象中。

本文将深入剖析 CDI 的关键特性,并演示在客户端类中实现依赖注入的多种方式。面向的是已有一定经验的开发者,所以基础概念一带而过,重点放在实战和踩坑点上。


2. 手动依赖注入(DYDI)

不借助任何框架,我们也能实现依赖注入,这种方式常被称为 DYDI(Do-it-Yourself Dependency Injection)

核心思想很简单:通过工厂或构建器,把依赖传给客户端类,从而解耦对象创建逻辑。

举个例子:

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

⚠️ DYDI 适合简单场景。但一旦项目变大,对象关系复杂,你会被一堆工厂类淹没,代码重复且难以维护。

❌ 这不是可扩展的方案。

那有没有更好的方式?当然有 —— CDI 就是为此而生。


3. 一个简单示例

CDI 把依赖注入变得极其简单:只需用几个注解标记服务类,在客户端定义注入点即可。

我们以一个图像编辑应用为例,支持打开、编辑、保存 GIF、JPG、PNG 文件。

3.1. beans.xml 文件

第一步:在 src/main/resources/META-INF/ 目录下创建 beans.xml 文件。

⚠️ 即使文件为空,也必须存在,否则 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>

💡 Java SE 环境下使用 Weld 时,这个文件是启动 CDI 的“开关”。


3.2. 服务类

定义图像编辑接口及其实现:

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 实现
}
public class PngFileEditor implements ImageFileEditor {
    // PNG 实现
}

3.3. 客户端类

客户端类通过构造器注入 ImageFileEditor

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

@Inject 是 CDI 的核心注解,用于定义注入点。

它支持三种注入方式:

  • 构造器注入(推荐,不可变)
  • 字段注入
  • Setter 注入

3.4. 使用 Weld 构建对象图

我们不依赖 Java EE 容器,而是用 Weld(CDI 的参考实现)在 Java SE 环境下运行:

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();
}

运行后,CDI 会抛出异常:

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

⚠️ 原因:CDI 不知道该注入哪个 ImageFileEditor 实现 —— 这叫 歧义注入异常(ambiguous injection)


3.5. @Default@Alternative 注解

解决歧义的最简单方式:使用 @Alternative 标记非默认实现。

CDI 默认会给所有实现类加上 @Default,所以我们需要显式排除某些实现:

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

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

public class PngFileEditor implements ImageFileEditor { ... } // 默认注入这个

✅ 此时运行程序,输出:

Opening PNG file file1.png

💡 小技巧:只需切换 @Alternative 的位置,就能轻松切换运行时注入的实现,适合快速切换策略或测试。


4. 字段注入

CDI 支持字段注入,用法简单粗暴:

@Inject
private final ImageFileEditor imageFileEditor;

⚠️ 注意:

  • 建议字段用 final,但必须配合构造器注入(字段注入不支持 final 初始化)
  • 字段注入破坏封装,不推荐在复杂场景使用

5. Setter 注入

Setter 注入也很直观:

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

✅ 适用于可选依赖或需要后期替换的场景。


6. @Named 注解

除了 @Default@Alternative,CDI 还支持通过名字注入 —— 使用 @Named

这种方式更语义化,适合需要明确指定实现的场景:

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

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

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

然后在客户端指定名字:

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

字段和 Setter 注入也支持:

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

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

⚠️ @Named 是字符串绑定,容易拼错,且重构不安全,建议只在简单场景或演示中使用。


7. @Produces 注解

有些服务需要复杂初始化(比如带配置参数),这时 @Produces 就派上用场了。

它用于创建工厂方法,返回已初始化的实例。

举个例子:我们加一个 TimeLogger 服务,记录操作时间:

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

TimeLogger 需要 SimpleDateFormatCalendar

public class TimeLogger {
    private SimpleDateFormat dateFormat;
    private Calendar calendar;
    
    public TimeLogger(SimpleDateFormat dateFormat, Calendar calendar) {
        this.dateFormat = dateFormat;
        this.calendar = calendar;
    }
    
    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

如何让 CDI 知道怎么创建 TimeLogger?用 @Produces

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

✅ CDI 会在需要 TimeLogger 时,自动调用这个工厂方法。

运行输出:

Opening PNG file file1.png at: 17:46

💡 @Produces 还支持销毁方法(@Disposes),用于资源清理。


8. 自定义限定符(Custom Qualifiers)

@Named 是字符串绑定,不够类型安全。CDI 提供了更强大的方案:自定义限定符

它不仅能绑定语义名称,还能携带元数据(如 RetentionPolicyElementType)。

定义限定符

@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 {}

绑定实现

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

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

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

注入时使用

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

✅ 运行结果与之前一致。

优势

  • ✅ 类型安全:编译期检查,避免拼写错误
  • ✅ 可携带元数据,支持更复杂的绑定逻辑
  • ✅ 比 @Default / @Alternative 更灵活,适合大型项目

⚠️ 重要规则:如果只有子类型被限定符标记,CDI 只会注入该子类型,不会回退到基类。


9. 总结

CDI 真正做到了“依赖注入无脑化”—— 多写几个注解,换来的是清晰、可维护的依赖管理。

✅ 优势:

  • 类型安全
  • 生命周期管理
  • 支持多种注入方式
  • 扩展性强(自定义限定符、生产者等)

⚠️ 注意:

  • 简单项目用 DYDI 更轻量
  • @Named 尽量少用,优先选自定义限定符
  • beans.xml 别忘了加

所有示例代码已托管至 GitHub:https://github.com/tech-tutorial/cdi-demo(模拟地址)


原始标题:An Introduction to CDI in Java