使用 CDI 中的注入和限定符

撰稿人:Andy Gibson

此页上的内容适用于 NetBeans IDE 7.2、7.3、7.4 和 8.0

JSR-299 指定的上下文和依赖关系注入 (CDI) 是 Java EE 6 的一个组成部分,提供了一个体系结构,以允许 Java EE 组件(例如 Servlet、企业 Bean 和 JavaBeans)在具有明确定义范围的应用程序生命周期内存在。此外,CDI 服务允许 Java EE 组件(例如 EJB 会话 Bean 和 JavaServer Faces (JSF) 受管 Bean)注入并通过触发和观察事件以松散耦合的方式进行交互。

本教程基于 Andy Gibson 发布的博客,标题为:CDI 入门指南,第 2 部分 - 注入。它演示了如何使用 CDI 注入来将类或接口注入至其他类。还显示了如何将 CDI 限定符应用到代码,以便可以指定应该在给定注入点注入的类类型。

NetBeans IDE 为上下文和依赖关系注入提供了内置支持,包括在项目创建时构建 beans.xml CDI 配置文件的选项,为标注提供的编辑器和导航支持,以及用于创建常用 CDI 工件的各种向导。


要学完本教程,您需要具备以下软件和资源。

软件或资源 要求的版本
NetBeans IDE 7.2、7.3、7.4、8.0、Java EE 版本
Java 开发工具包 (JDK) 版本 7 或 8
GlassFish Server Open Source Edition 3.x 或 4.x
cdiDemo.zip N/A

注:

  • NetBeans IDE Java 包中还含 GlassFish Server Open Source Edition,后者是与 Java EE 兼容的容器。
  • 可以下载本教程的解决方案样例项目:cdiDemo2.zip

注入:CDI 中的 "I"

CDI 是一个用于注入上下文和依赖关系的 API。在 Seam 和 Spring 中,依赖关系主要通过命名 Bean 并根据名称将它们绑定到注入点以发挥作用。如果是在完成上下文和依赖关系注入以及 JSF 2.0 入门指南以后开始学习本教程,则使用 @Named 标注定义 Bean 名称时,仅限从 JSF 页按名称引用受管 Bean。@Named 标注的主要作用是定义 Bean,以在应用程序内解析 EL 语句(通常是通过 JSF EL 解析器完成)。可以通过使用名称执行注入,但 CDI 注入并非按照此方式工作,因为 CDI 表示注入点和要注入的 Bean 的方式更为丰富。

在以下示例中,将创建 ItemProcessor,该程序从实现 ItemDao 接口的类获得项列表。使用 CDI 的 @Inject 标注演示如何将 Bean 注入另一个类。下图描绘您在本练习中构建的方案。

此 CDI 图显示了在本练习中创建的对象

DAO 表示数据访问对象

  1. 首先,从 cdiDemo.zip 文件提取样例启动项目(请参见上面的所需资源列表)。在 IDE 中打开项目,方法是选择 "File"(文件)> "Open Project"(打开项目)(Ctrl-Shift-O 组合键;在 Mac 上为 ⌘-Shift-O 组合键),然后从计算机上的相应位置选择该项目。
  2. 右键单击“项目”窗口中的项目节点,然后选择“属性”。
  3. 选择 "Run"(运行)类别,并确认在 "Server"(服务器)下拉列表中选定 GlassFish 实例。
  4. 创建新 Item 类,并将其存储在名为 exercise2 的新包中。单击 "New File"(新建文件)("New File"(新建文件)按钮) 按钮,或者按 Ctrl-N 组合键(在 Mac 上为 ⌘-N 组合键)以打开文件向导。
  5. 选择 "Java" 类别,然后选择 "Java Class"(Java 类)。单击 "Next"(下一步)。
  6. 输入 Item 作为类名,然后键入 exercise2 作为包。(新包在完成向导时创建。)
    Java 类向导
  7. 单击 "Finish"(完成)。新类和包生成,并在编辑器中打开 Item 类。
  8. Item POJO 创建 valuelimit 属性,并实现 toString() 方法。在该类中添加以下内容。
    public class Item {
    
        private int value;
        private int limit;
    
        @Override
        public String toString() {
            return super.toString() + String.format(" [Value=%d, Limit=%d]", value,limit);
        }
    }
  9. 在该类中添加 getter 和 setter 方法。要执行此操作,请确保将光标置于类定义之间(例如,该类的花括号之间),然后右键单击编辑器并选择 "Insert Code"(插入代码)(Alt-Insert;在 Mac 上为 Ctrl-I)。选择 "Getter and Setter"(Getter 和 Setter)。
    "Insert Code"(插入代码)弹出式窗口
  10. 选中 Item 复选框(执行此操作可选择该类中包含的所有属性)。
    "Generate Getters and Setters"(生成 getter 和 setter)对话框
  11. 单击 "Generate"(生成)。将为该类生成 getter 和 setter 方法。
    public class Item {
    
        private int value;
        private int limit;
    
        public int getLimit() {
            return limit;
        }
    
        public void setLimit(int limit) {
            this.limit = limit;
        }
    
        public int getValue() {
            return value;
        }
    
        public void setValue(int value) {
            this.value = value;
        }
    
        @Override
        public String toString() {
            return super.toString() + String.format(" [Value=%d, Limit=%d]", value, limit);
        }
    }
  12. 创建同时具有 valuelimit 参数的构造函数。同样,IDE 可以帮助完成此操作。在类定义内按 Ctrl-空格键,并选择 Item(int value, int limit) - generate 选项。
    显示在编辑器中的代码完成列表
    下列构造函数将添加到类中。
    public class Item {
    
        public Item(int value, int limit) {
            this.value = value;
            this.limit = limit;
        }
    
        private int value;
        private int limit;
    
        ...
  13. 创建 ItemDao 接口以定义获取 Item 对象列表的方式。在此测试应用程序中,预期将使用多个实现,因此将编写多个接口的代码。

    单击 "New File"(新建文件)("New File"(新建文件)按钮) 按钮,或者按 Ctrl-N 组合键(在 Mac 上为 ⌘-N 组合键)以打开文件向导。

  14. 选择 "Java" 类别,然后选择 "Java Interface"(Java 接口)。单击 "Next"(下一步)。
  15. 键入 ItemDao 作为类名,然后输入 exercise2 作为包。
  16. 单击 "Finish"(完成)。将会生成新接口并在编辑器中将其打开。
  17. 添加名为 fetchItems() 的方法,它将返回 Item 对象的 List
    public interface ItemDao {
    
        List<Item> fetchItems();
    
    }
    (使用编辑器的提示为 java.util.List 添加 import 语句。)
  18. 创建 ItemProcessor 类。这是要向其中注入 Bean 并从中执行进程的主类。目前,您将从 DAO 入手,了解如何将其注入我们的处理器 Bean。

    单击 "New File"(新建文件)("New File"(新建文件)按钮) 按钮,或者按 Ctrl-N 组合键(在 Mac 上为 ⌘-N 组合键)以打开文件向导。

  19. 选择 "Java" 类别,然后选择 "Java Class"(Java 类)。单击 "Next"(下一步)。
  20. 键入 ItemProcessor 作为类名,然后输入 exercise2 作为包。单击 "Finish"(完成)。

    将会生成新类并在编辑器中将其打开。

  21. 修改该类,如下所示:
    @Named
    @RequestScoped
    public class ItemProcessor {
    
        private ItemDao itemDao;
    
        public void execute() {
            List<Item> items = itemDao.fetchItems();
            for (Item item : items) {
                System.out.println("Found item " + item);
            }
        }
    }
  22. 修复导入。在编辑器中右键单击并选择 "Fix Imports"(修复导入),或者按 Ctrl-Shift-I 组合键(在 Mac 上按 ⌘-Shift-I 组合键)。
    "Fix Imports"(修复导入)对话框
  23. 单击 "OK"(确定)。需要以下类的 import 语句:
    • java.util.List
    • javax.inject.Named
    • javax.enterprise.context.RequestScoped
  24. 首先是一个简单的 DAO,仅用于创建项列表并返回项的固定列表。

    在 "Projects"(项目)窗口中,右键单击 exercise2 包节点并选择 "New"(新建)> "Java Class"(Java 类)。在 Java 类向导中,将类命名为 DefaultItemDao。单击 "Finish"(完成)。Java 类向导
  25. 在编辑器中,让 DefaultItemDao 实现 ItemDao 接口,然后提供 fetchItems() 实现。
    public class DefaultItemDao implements ItemDao {
    
        @Override
        public List<Item> fetchItems() {
            List<Item> results = new ArrayList<Item>();
            results.add(new Item(34, 7));
            results.add(new Item(4, 37));
            results.add(new Item(24, 19));
            results.add(new Item(89, 32));
            return results;
        }
    }
    (按 Ctrl-Shift-I 组合键(在 Mac 上按 ⌘-Shift-I 组合键)为 java.util.Listjava.util.ArrayList 添加 import 语句。)
  26. 切换到 ItemProcessor 类(按 Ctrl-Tab 组合键)。为了将 DefaultItemDao 注入到 ItemProcessor,我们向 ItemDao 字段添加 javax.inject.Inject 标注以表示该字段为注入点。
    import javax.inject.Inject;
    ...
    
    @Named
    @RequestScoped
    public class ItemProcessor {
    
        @Inject
        private ItemDao itemDao;
    
        ...
    }
    使用编辑器的代码完成支持向类中添加 @Inject 标注和 import 语句。例如,键入 @Inj,按后按 Ctrl-空格组合键。
  27. 最后,需要采用一些方式来调用 ItemProcessor 上的 execute() 方法。此方法可以在 SE 环境中运行,但现在会将其保留在 JSF 页。创建名为 process.xhtml 的新页,并包含用于调用 execute() 方法的按钮。

    单击 "New File"(新建文件)("New File"(新建文件)按钮) 按钮,或者按 Ctrl-N 组合键(在 Mac 上为 ⌘-N 组合键)以打开文件向导。
  28. 选择 "JavaServer Faces" 类别,然后选择 "JSF Page"(JSF 页)。单击 "Next"(下一步)。
  29. 键入 process 作为文件名,然后单击 "Finish"(完成)。
    JSF 页向导
  30. 在新的 process.xhtml 文件中,添加连接到 ItemProcessor.execute() 方法的按钮。使用 EL 时,受管 Bean 的默认名称与类名称相同,但是第一个字母用小写(例如,itemProcessor)。
    <h:body>
        <h:form>
            <h:commandButton action="#{itemProcessor.execute}" value="Execute"/>
        </h:form>
    </h:body>
  31. 运行此项目之前,在项目的 Web 部署描述符中将 process.xhtml 文件设置为新的欢迎页面。

    使用 IDE 的 "Go to File"(转至文件)对话框快速打开 web.xml 文件。从 IDE 的主菜单中选择 "Navigate"(导航)> "Go to File"(转至文件)(Alt-Shift-O;在 Mac 上为 Ctrl-Shift-O),然后键入 "web"。
    "Go to File"(转至文件)对话框
  32. 单击 "OK"(确定)。在 web.xml 文件的 XML 视图中,进行以下更改。
    <welcome-file-list>
        <welcome-file>faces/process.xhtml</welcome-file>
    </welcome-file-list>
  33. 在 IDE 的主工具栏中单击 "Run Project"(运行项目)("Run Project"(运行项目)按钮) 按钮。编译该项目并将其部署到 GlassFish,然后在浏览器中打开 process.xhtml 文件。
  34. 单击页面上显示的 Execute 按钮。切换回 IDE 并检查 GlassFish Server 日志。服务器日志会显示在 "Output"(输出)窗口(Ctrl-4 组合键;在 Mac 上为 ⌘-4 组合键)中 "GlassFish Server" 标签的下方。单击该按钮时,日志将列出默认 DAO 实现的项。
    "Output"(输出)窗口中的 GlassFish Server 日志
    在 "Output"(输出)窗口中右键单击,然后选择 "Clear"(清除)(Ctrl-L 组合键;在 Mac 上为 ⌘-L 组合键)以清除日志。在上图中,仅在单击 Execute 按钮前清除日志。

我们创建了一个实现 ItemDao 接口的类,然后在部署应用程序时,由 CDI 实现来处理模块中的受管 Bean(由于模块中的 beans.xml 文件)。@Inject 标注指定要将受管 Bean 注入该字段,而我们只知道可注入 Bean 必须实现 ItemDao 或该接口的一些子类型。在这种情况下,DefaultItemDao 类非常符合要求。

如果注入了多个 ItemDao 实现,会怎么样?CDI 可能不知道应该选择哪个实现,会标记部署时错误。要解决此问题,需要使用 CDI 限定符。限定符将在以下部分进行探讨。


使用限定符

CDI 限定符是一个标注,可在类级别应用以表示该类所属的 Bean 类型,还可以在字段级别(在其他位置)应用以表示该点需要注入哪种类型的 Bean。

为了演示在我们构建的应用程序中需要限定符,我们向还会实现 ItemDao 接口的应用程序中添加另一个 DAO 类。下图描述了本练习中将要构建的方案。CDI 必须能够确定在注入点应该使用哪个 Bean 实现。因为有两个 ItemDao 实现,我们可以通过创建名为 Demo 的限定符来解决此问题。然后,使用 @Demo 标注对要使用的 Bean 以及 ItemProcessor 中的注入点添加“标记”。

此 CDI 图显示在本教程中创建的对象

请执行以下步骤。

  1. 在 "Projects"(项目)窗口中,右键单击 exercise2 包,并选择 "New"(新建)> "Java Class"(Java 类)。
  2. 在新建 Java 类向导中,将新类命名为 AnotherItemDao,然后单击 "Finish"(完成)。将会生成新类并在编辑器中将其打开。
  3. 按如下方式修改类,以实现 ItemDao 接口,并定义该接口的 fetchItems() 方法。
    public class AnotherItemDao implements ItemDao {
    
        @Override
        public List<Item> fetchItems() {
            List<Item> results = new ArrayList<Item>();
            results.add(new Item(99, 9));
            return results;
        }
    }

    请务必为 java.util.Listjava.util.ArrayList 添加 import 语句。要执行此操作,请在编辑器中右键单击,然后选择 "Fix Imports"(修复导入),或者按 Ctrl-Shift-I 组合键(在 Mac 上按 ⌘-Shift-I 组合键)。

    现在有两个实现 ItemDao 的类,因此无法确定要注入哪个 Bean。

  4. 单击 "Run Project"(运行项目)("Run Project"(运行项目)按钮) 按钮以运行项目。请注意,项目现在无法部署。

    您可能只需要保存文件,因为 "Deploy on Save"(在保存时部署)默认为启用状态,IDE 将自动部署项目。

  5. 在 "Output"(输出)窗口(Ctrl-4 组合键;在 Mac 上为 ⌘-4 组合键)中查看服务器日志。将会显示类似如下的错误消息。
    Caused by: org.jboss.weld.DeploymentException: Injection point has ambiguous dependencies.
    Injection point: field exercise2.ItemProcessor.itemDao;
    Qualifiers: [@javax.enterprise.inject.Default()];
    Possible dependencies: [exercise2.DefaultItemDao, exercise2.AnotherItemDao]

    要在 "Output"(输出)窗口中将文本调整为多行,请右键单击并选择 "Wrap text"(自动换行)。此操作不需要水平滚动。

    Weld(CDI 实现)提供了一个不明确的依赖关系错误含义,它不能确定为给定注入点使用哪个 Bean。在 Weld 中,CDI 注入可能发生的绝大多数(如果不是所有)错误会在部署时报告,甚至包含钝化 Bean 是否会丢失 Serializable 实现。

    可以指定 ItemProcessor 中的 itemDao 字段作为一个特定类型与其中一个实现类型(AnotherItemDaoDefaultItemDao)匹配,因为它只会与一个类类型匹配。但是,以后将不能对接口进行编码,也很难在不更改字段类型的情况下更改实现。查看 CDI 限定符是更好的解决方法。

    当 CDI 检查注入点以找到合适的注入 Bean 时,它会同时考虑类类型和任何限定符。在不知道的情况下,我们已经使用了一个名为 @Any 的默认限定符。现在需要创建一个 @Demo 限定符以应用于 DefaultItemDao 实现,以及 ItemProcessor 中的注入点。

    IDE 提供可用于生成 CDI 限定符的向导。

  6. 单击 "New File"(新建文件)("New File"(新建文件)按钮) 按钮,或者按 Ctrl-N 组合键(在 Mac 上为 ⌘-N 组合键)以打开文件向导。
  7. 选择 "Context and Dependency Injection"(上下文和依赖关系注入)类别,然后选择 "Qualifier Type"(限定符类型)。单击 "Next"(下一步)。
  8. 输入 Demo 作为类名,然后输入 exercise2 作为包名。
  9. 单击 "Finish"(完成)。新 Demo 限定符在编辑器中打开。
    package exercise2;
    
    import static java.lang.annotation.ElementType.TYPE;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.inject.Qualifier;
    
    /**
    *
    * @author nbuser
    */
    @Qualifier
    @Retention(RUNTIME)
    @Target({METHOD, FIELD, PARAMETER, TYPE})
    public @interface Demo {
    }

    接下来,在类级别将此限定符添加到默认 DAO 实现。

  10. 在编辑器中切换到 DefaultItemDao(按 Ctrl-Tab 组合键),然后在类定义上方键入 @Demo
    @Demo
    public class DefaultItemDao implements ItemDao {
    
    @Override
    public List<Item> fetchItems() {
        List<Item> results = new ArrayList<Item>();
        results.add(new Item(34, 7));
        results.add(new Item(4, 37));
        results.add(new Item(24, 19));
        results.add(new Item(89, 32));
        return results;
    }
    }
    键入 @ 后,按 Ctrl-空格键以调用代码完成建议。编辑器识别 Demo 限定符并列出 @Demo 作为代码完成选项。
  11. 单击 "Run Project"(运行项目)("Run Project"(运行项目)按钮) 按钮以运行项目。该项目将构建和部署,且不出现错误。

    注:对于此项修改,可能需要显式运行项目以重新部署应用程序而不是增量部署所做的更改。

  12. 在浏览器中,单击 Execute 按钮,然后返回至 IDE 并检查 "Output"(输出)窗口中的服务器日志。将看到以下输出结果。
    INFO: Found item  [Value=99, Limit=9]

    输出列出了 AnotherItemDao 类中的项。请回想一下,我们对 DefaultItemDao 实现进行了标注,但没有对 ItemProcessor 中的注入点进行标注。通过向默认 DAO 实现添加 @Demo 限定符,使另一个实现更加匹配注入点,因为该实现同时与类型和限定符匹配。当前,DefaultItemDaoDemo 限定符,而注入点上没有,因此降低了适用性。

    接下来,将向 ItemProcessor 中的注入点添加 @Demo 标注。

  13. 在编辑器中切换到 ItemProcessor(按 Ctrl-Tab 组合键),然后进行以下更改。
    @Named
    @RequestScoped
    public class ItemProcessor {
    
    @Inject @Demo
    private ItemDao itemDao;
    
    public void execute() {
        List<Item> items = itemDao.fetchItems();
        for (Item item : items) {
            System.out.println("Found item " + item);
        }
    }
    }
  14. 在浏览器中,单击 Execute 按钮,然后返回至 IDE 并检查 "Output"(输出)窗口中的服务器日志。您会再次看到默认实现 (DefaultItemDao) 的输出。
    INFO: Found item  [Value=34, Limit=7]
    INFO: Found item  [Value=4, Limit=37]
    INFO: Found item  [Value=24, Limit=19]
    INFO: Found item  [Value=89, Limit=32]

    这是因为现在是根据类型限定符进行匹配,且 DefaultItemDao 是唯一同时具有正确类型和 @Demo 标注的 Bean。


其他注入方法

有多种方式可以在注入的类上定义注入点。到目前为止,您对引用注入对象的字段添加了标注。不需要为字段注入提供 getter 和 setter。如果要使用最终字段创建不可变受管 Bean,可以通过使用 @Inject 标注对构造函数进行标注,在构造函数中使用注入。然后,可以对构造函数参数应用任何标注以限定要注入的 Bean。(当然,每个参数都有一个类型可帮助限定要注入的 Bean。)Bean 可能只有一个定义了注入点的构造函数,但是可以实现多个构造函数。

@Named
@RequestScoped
public class ItemProcessor {

    private final ItemDao itemDao;

    @Inject
    public ItemProcessor(@Demo ItemDao itemDao) {
        this.itemDao = itemDao;
    }
}

此外,还可以调用初始化方法,为该方法传递要注入的 Bean。

@Named
@RequestScoped
public class ItemProcessor {

    private ItemDao itemDao;

    @Inject
    public void setItemDao(@Demo ItemDao itemDao) {
        this.itemDao = itemDao;
    }
}

虽然在上例中使用了 setter 方法进行初始化,但您可以创建任何方法,并使用它对方法调用中任意数量的 Bean 进行初始化。您还可以在一个 Bean 中使用多个初始化方法。

@Inject
public void initBeans(@Demo ItemDao itemDao, @SomeQualifier SomeType someBean) {
    this.itemDao = itemDao;
    this.bean = someBean;
}

无论注入点是如何定义的,都可以将同样的规则应用于匹配的 Bean。CDI 会尝试根据类型和限定符查找最佳匹配,并且会在注入点有多个匹配 Bean 或没有匹配 Bean 时部署失败。


另请参见

请继续完成本系列中关于上下文和依赖关系注入的下一个部分:

有关 CDI 和 Java EE 的详细信息,请参见以下资源。

get support for the NetBeans

Support


By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2013, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo