编写简单的 MicroProfile 应用程序,第 4 部分

使用 WebSocket 和 CDI 事件

集成 WebSocket 与 CDI bean 以通知客户端相关更改

Comments

系列内容:

此内容是该系列 4 部分中的第 # 部分: 编写简单的 MicroProfile 应用程序,第 4 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:编写简单的 MicroProfile 应用程序,第 4 部分

敬请期待该系列的后续内容。

概述

在本教程中,添加 WebSocket 并使用 CDI 事件来集成 WebSocket 和 CDI bean,以便服务器可以通知客户端相关更改。

如果您观察敏锐,可能会注意到关于应用程序的这一点:只有当启动会议时(而不是它已在运行时),重定向才有效。为了使连接操作有效,浏览器需要找出会议何时发生变化。通过轮询服务器可完成此操作,但这可能成本过高。可以改用 WebSocket 来允许服务器通知客户端。这减少了向服务器发出的请求数量,并且迅速向客户端提供更新。

改编自博客文章:“编写简单的 MicroProfile 应用程序:使用 WebSocket 和 CDI 事件。”

前提条件

  • 已完成“编写简单的 MicroProfile 应用程序,第 3 部分:使用 Java EE 并发性”。
  • Eclipse IDE for Web Developers:运行安装程序并选择 Eclipse IDE for Java EE Developers。

    注意:这些步骤在 Linux 和 Liberty Developer Tools 18.0.0.3 上运行的 2018-09 版 Eclipse 上进行了测试。

    注意:如果遇到类似于 Could not initialize class org.codehaus.plexus.archiver.jar.JarArchiver 的错误消息,可参阅“故障排除”部分。

  • IBM Liberty Developer Tools (WDT)
    1. 启动 Eclipse。
    2. 启动 Eclipse Marketplace:Help > Eclipse Marketplace
    3. 搜索 IBM Liberty Developer Tools,在已选中默认配置的情况下单击 Install
  • Git
  • 安装 IBM Cloud CLI

步骤

1 检出源代码

  1. 从命令行运行以下命令:
    git clone https://github.com/IBM/microprofile-meeting-concurrency.git
  2. 在 Eclipse 中,将项目作为现有项目导入。
    1. 在 Eclipse 中,切换至 Git 透视图。
    2. 单击 Git Repositories 视图中的 Clone a Git repository
    3. 输入 URI:https://github.com/IBM/microprofile-meeting-concurrency.git
    4. 单击 Next,然后再次单击 Next,接受默认值。
    5. Initial branch 下拉列表中,单击 master
    6. 选择 Import all existing Eclipse projects after clone finishes,然后单击 Finish
    7. 切换至 Java EE 透视图。
    8. 在 Project Explorer 视图中会自动创建会议项目。

2 安装 MongoDB

如果您已完成先前的教程并且安装了 MongoDB,确保 MongoDB 正在运行。如果要重新开始,确保安装 MongoDB。根据您所使用的平台,安装说明可能会有所不同。对于本练习,您应该从 mongoDB 下载中心获取 MongoDB 的社区版本。

在安装后,可以使用以下命令运行 MongoDB 数据库守护程序:

mongod -dbpath <path to database>

数据库必须正在运行,应用程序才能正常工作。如果数据库未在运行,服务器日志中会有很多噪音数据。

3 更新应用程序以根据 WebSocket API 进行编译

要开始编写代码,必须更新 Maven pom.xml,指明对 WebSocket API for Java EE 的依赖:

  1. 在 Eclipse 中打开 pom.xml
  2. 在编辑器中,选择 Dependencies 选项卡。
  3. Dependencies 选项卡上有两个部分,一个部分用于 Dependencies,另一个部分用于 Dependency Management。在 Dependencies 框的右侧,有一个 Add 按钮。单击 Add 按钮。
  4. groupdId 中输入 javax.websocket
  5. artifactId 中输入 javax.websocket-api
  6. version 中输入 1.1
  7. scope 下拉列表中选择 provided。这将允许应用程序进行编译,但会阻止 Maven WAR 打包器将 API 放入 WAR 文件中。稍后将配置构建,使其可供服务器使用。
  8. 单击 OK
  9. 保存 pom.xml

4 创建 CDI 限定符

CDI 限定符只是一个标注有 @Qualifier 的注解。然后,这可以与其他 CDI 注解一起使用以影响行为。对于 CDI 事件而言,它将事件生产者链接到事件消费者。

  • 右键单击 meetings 项目,然后单击 New > Annotation...
  • 输入名称 MeetingEvent
  • 单击 Finish
  • 应将注解添加到类型名称。这里有三个关键注解。第一个注解是 Qualifier,它指示注解是 CDI 限定符:
    @Qualifier
    public @interface MeetingEvent {
    }

    这引入了 javax.inject 包中的新类型 Qualifier

    import javax.inject.Qualifier;

    第二个注解是 Retention,它指示注解应在运行时可用。这将允许 CDI 运行时对它们进行处理:

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)

    这引入了两个新类型:RetentionRetentionPolicy。它们位于 java.lang.annotation 包中:

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;

    最后一个注解是 Target,指示可以应用注解的位置。对于 CDI 限定符,需要将其应用于字段和参数:

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER})

    这引入了两个新类型:TargetElementType。它们位于 java.lang.annotation 包中:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Target;
  • 保存该文件。

    注解应该类似如下:

    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    public @interface MeetingEvent {
    
    }

5 创建 CDI 事件对象

通过 CDI 事件,您可以在生产者和消费者之间传递所需的任何对象,但对于此应用程序,我们将创建一个对象。要传递两项内容:一项是用于识别会议的事件,另一项是会议的 URL。

执行以下操作来创建事件对象:

  1. 右键单击 meetings 项目,然后单击 New > Class…
  2. Package 字段中,输入 net.wasdev.samples.microProfile.meetings
  3. 输入名称 MeetingStartEvent
  4. 单击 Finish
  5. 添加 String 字段以存储 ID。
    private String id;
  6. 添加 String 字段以存储 URL。
    private String url;
  7. 接下来,添加一个构造函数,它抓取 idurl 的值,并将其存储在字段中:
    public MeetingStartEvent(String id, String url) {
        this.id = id;
        this.url = url;
    }
  8. 最后,创建简单的 getter 以返回字段:
    public String getId() {
       return id;
    }
    
    public String getUrl() {
       return url;
    }
  9. 保存该文件。

6 在会议启动时发送事件

下一部分是获取 MeetingManager,以在启动会议时发出事件:

  1. 打开 MeetingManager 类。
  2. 添加新字段以注入 CDI Event 类。使用要设置的事件对象对 Event 类进行参数化。同时还应使用我们先前创建的 CDI 限定符 MeetingEvent 对该字段进行注解。
    @Resource
    private ManagedScheduledExecutorService executor;
    @Inject
    @MeetingEvent
    private Event<MeetingStartEvent> events;

    这引入了 javax.enterprise.event 包中的新类型 Event。同时还从 javax.inject 包中引入了 Inject

    import javax.enterprise.event.Event;
    import javax.inject.Inject;
  3. 查找 startMeeting 方法。在此方法构造的末尾,MeetingStartEvent 的新实例传入了会议 ID 和 URL:
    MeetingStartEvent eventObject = new MeetingStartEvent(id, url);
  4. 然后,调用传入事件对象的 Event fire 对象:
    events.fire(eventObject);
  5. 保存该文件。

在此阶段,可以运行应用程序。将发出事件,但不会发生任何事情,因为没有什么可以接收事件。

7 创建 WebSocket

WebSocket 将处理浏览器与服务器之间的连接,并且将接收会议启动事件。浏览器将发送会议 ID,WebSocket 将通知它何时启动会议。

执行以下操作来创建 WebSocket:

  1. 右键单击 meetings 项目,然后单击 New > Class…
  2. Package 字段中,输入 net.wasdev.samples.microProfile.meetings
  3. 输入名称 MeetingNotifier
  4. 单击 Finish
  5. 根据规范,WebSocket 组件不是 CDI bean。为确保 CDI 可以识别 bean,需要对其进行注解。在此情况下,我们向类型添加 Dependent 注解:
    @Dependent
    public class MeetingNotifier {
    }

    这引入了新类 Dependent,它位于 javax.enterprise.context 包中:

    import javax.enterprise.context.Dependent;
  6. 要使类进入 WebSocket,就需要使用 ServerEndpoint 注解对其进行标注。该注解采用 URL 路径,用于对其进行调用。URL 路径必须以正斜杠开头:
    @Dependent
    @ServerEndpoint("/notifier")

    这引入了新类 ServerEndpoint,它位于 javax.websocket.server 包中。在导入时,由于存在多个 ServerEndpoint 类,须注意导入正确的类:

    import javax.websocket.server.ServerEndpoint;
  7. 保存该文件。

    类型定义现在应该类似如下:

    @Dependent
    @ServerEndpoint("/notifier")
    public class MeetingNotifier {
  8. WebSocket 将需要与 MeetingManager 进行交互,因此需要将其注入字段中:
    public class MeetingNotifier {
    @Inject
    private MeetingManager manager;

    这从 javax.inject 包中引入了新类型 Inject

    import javax.inject.Inject;
  9. 对于每个 WebSocket 连接,WebSocket 容器可管理类的实例。但是,当 CDI 事件系统分发事件时,它会创建一个新实例。因此,需要存储 WebSocket Session 对象以供日后使用。Map 用于存储与会议关联的 Session 对象。由于将有多个 Session 对象,因此适合使用 Session 对象的 Collection。当然,因为这将需要处理多个线程,我们使用会话的并发版本(添加到 MeetingNotifier 类的下一行):
    private static ConcurrentMap<String, Queue<Session>> listeners = new ConcurrentHashMap<();

    这引入了四个新类:ConcurrentMapConcurrentHashMap 类位于 java.util.concurrent 包中;Queue 类位于 java.util 包中;Session 位于 javax.websocket 包中。

    import java.util.Queue;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    import javax.websocket.Session;
  10. ServerEndpoint 上有多种方法。但对于此情况,关键方法是 onMessage 方法:

    使用 OnMessage 注解指示 onMessage 方法。可以使用多种方法签名。但在此情况下,该方法将采用 String,其中包含会议 ID 和 WebSocket 会话:

    @OnMessage
    public void onMessage(String id, Session s) throws IOException {
        // code will go in here
    }

    这引入了新类型:javax.websocket 包中的 OnMessage 注解,以及来自 java.ioIOException

    import javax.websocket.OnMessage;
    import java.io.IOException;
  11. 首先检查该 ID 是否确实用于会议。如果没有具有该 ID 名称的会议,那么该方法应退出:
    JsonObject m = manager.get(id);
    if (m == null) {
        s.close();
        return;
    }

    这引入了新类 JsonObject,它位于 javax.json 包中:

    import javax.json.JsonObject;
  12. 接下来获取会议的 URL:
    JsonString url = m.getJsonString("meetingURL");

    这引入了新类 JsonString,它位于 javax.json 包中:

    import javax.json.JsonString;
  13. 如果已有会议 URL,应直接将信息发送至 WebSocket 客户端,然后该方法应退出。要将信息发送至客户端,可使用会话:获取远程对象,然后发送一些文本。JsonStringtoString 方法会用引号括起 URL,因此必须使用 getString 方法:
    if (url != null) {
        s.getBasicRemote().sendText(url.getString());
        s.close();
        return;
    }
  14. 现在需要存储会话,以便在启动会议时通知客户端。这将存储在 Map 中。因此,首先要获取会话集合:
    Queue<Session> sessions = listeners.get(id);
    if (sessions == null) {
        // code will go here
    }
  15. 在 null 检查的内部,您需要创建一个新集合。这应该是并发集合,因此使用 ArrayBlockingQueue
    sessions = new ArrayBlockingQueue<>(1000);

    这引入了新类 ArrayBlockingQueue,它位于 java.util.concurrent 包中。

    import java.util.concurrent.ArrayBlockingQueue;
  16. 现在,需要将其放入 Map 中。当然,可能有两个客户端流经此方法。因此,使用 putIfAbsent 方法,而不是使用 put 进行覆盖:
    Queue<Session> actual = listeners.putIfAbsent(id, sessions);

    如果 put 成功,actual 将为 null。如果另一个线程获胜并将其 sessions 副本放入 Map 中,它将具有应使用的集合。这时需要一次交换:

    if (actual != null) {
        sessions = actual;
    }
  17. 在方法中(以及在 if 块的外部,对 sessions 进行 null 检查)需执行的最后一个操作是,将 Session 添加到 Session 对象的 Collection
    sessions.add(s);

    作为结果添加的代码应类似如下:

    Queue<Session> sessions = listeners.get(id);
    if (sessions == null) {
        sessions = new ArrayBlockingQueue<(1000);
        Queue<Session> actual = listeners.putIfAbsent(id, sessions);
        if (actual != null) {
            sessions = actual;
        }
    }
    sessions.add(s);
  18. 既然已存储会话,就需要定义事件方法。

    该方法的名称并不重要,但它必须接受该事件。需要使用 Observes 注解(指示这是一种事件通知方法)和 MeetingEvent 注解对接受该事件的参数加以标注,以便该参数了解要调用的事件种类:

    public void startMeeting(@Observes @MeetingEvent MeetingStartEvent event) {
       // add the notification code here
    }

    这引入了 javax.enterprise.event 包中的新类型 Observes

    import javax.enterprise.event.Observes;
  19. 如果调用此方法,那么会启动会议。由于已启动会议,因此不再需要对会话进行高速缓存。所以可以将其从 Map 中除去:
    Queue<Session> sessions = listeners.remove(event.getId());
  20. 当然,可能没有存储任何会话,此时它将为 null。只有当 sessions 不为 null 时,才会发生下一部分:
    if (sessions != null) {
       // add the next bit of code  here
    }
  21. 应该为每个会话完成此逻辑。所以,将执行一个简单的增强 for 循环:
    for (Session s : sessions) {
        // add the next bit of code here
    }
  22. 需要打开会话以将数据发送到客户端。因此,先检查这一项:
    if (s.isOpen()) {
        // add the next bit of code here
    }
  23. 最后,应将 URL 发送至客户端。这可能会导致 IOException,此方法无法抛出该异常,因此必须将其捕获:
    try {
           s.getBasicRemote().sendText(event.getUrl());
           s.close();
       } catch (IOException e) {
           e.printStackTrace();
       }
  24. 保存该文件。

您现在已对应用程序进行了编码。通过打开两个浏览器窗口来测试应用:一个用于连接会议,另一个用于启动会议。观看这两个浏览器窗口如何立即重定向。

8 配置 Liberty 以运行 WebSocket

  1. 通过 src > main > liberty > config > server.xml 打开 server.xml
  2. 查找 <feature manager> 元素。它应类似如下:
    <featureManager>
           <feature>mongodb-2.0</feature>
           <feature>concurrent-1.0</feature>
    </featureManager>
  3. </featureManager> 结束元素之前,添加 feature 元素, 并以 websocket-1.1 功能作为主体。
    <feature>websocket-1.1</feature>
  4. 保存该文件。

运行应用程序

可以使用两种方法从 WDT 中运行应用程序:

  • 第一种方法是使用 Maven 来构建和运行项目:
    • 运行 Maven install 目标来构建和测试项目:
      • 右键单击 meetings 项目中的 pom.xml
      • 单击 Run As… > Maven Build…
      • Goals 字段中,输入 install,然后单击 Run

        第一次运行此目标时,可能会花几分钟时间来下载 Liberty 依赖项。

    • liberty:start-server goal 运行 Maven 构建:
      • 右键单击 pom.xml
      • 单击 Run As… > Maven Build
      • Goals 字段中,输入 liberty:start-server,然后单击 Run

        这会在后台启动服务器。

    • 打开应用程序(地址为 http://localhost:9080/meetings/)。
    • 要再次停止服务器,可运行 liberty:stop-server 构建目标。
  • 第二种方法是右键单击 meetings 项目,并选择 Run As… > Run on Server。但如果这样做,须注意一些事项。WDT 不会像您期望的那样自动添加 MicroProfile 功能,因此您需要手动添加这些功能。此外,除非添加 include,否则不会选取 src/meain/liberty/config 中发生的任何配置更改。

GitHub

在 GitHub 中检出此项目的最终代码

本文翻译自:Write a simple MicroProfile application, Part 4: Use WebSockets and CDI events (2019-01-29)


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1065266
ArticleTitle=编写简单的 MicroProfile 应用程序,第 4 部分: 使用 WebSocket 和 CDI 事件
publish-date=03252019