后台处理

许多应用都需要在 Web 请求情境之外进行后台处理。本教程会创建一个 Web 应用,允许用户输入要翻译的文本,然后显示先前译文的列表。系统会在后台进程中执行翻译,以避免阻塞用户的请求。

下图演示了翻译请求过程。

架构图。

以下事件序列演示了本教程应用的工作原理:

  1. 访问网页,查看存储在 Firestore 中的先前译文列表。
  2. 通过输入 HTML 表单请求翻译文本。
  3. 翻译请求被发布到 Pub/Sub。
  4. 触发已订阅该 Pub/Sub 主题的 Cloud Run 服务。
  5. Cloud Run 服务使用 Cloud Translation 翻译文本。
  6. Cloud Run 服务将结果存储到 Firestore 中。

本教程适用于有兴趣了解如何使用 Google Cloud 进行后台处理的用户。无需拥有 Pub/Sub、Firestore、Cloud Run 相关经验。不过,具有一定程度的 Java 和 HTML 相关经验会有助于您了解所有代码。

目标

  • 了解和部署 Cloud Run 服务。
  • 试用该应用。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google Cloud 账号。如果您是 Google Cloud 新手,请创建一个账号来评估我们的产品在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. 确保您的 Google Cloud 项目已启用结算功能

  4. Enable the Firestore, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. Install the Google Cloud CLI.
  6. To initialize the gcloud CLI, run the following command:

    gcloud init
  7. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  8. 确保您的 Google Cloud 项目已启用结算功能

  9. Enable the Firestore, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  10. Install the Google Cloud CLI.
  11. To initialize the gcloud CLI, run the following command:

    gcloud init
  12. 更新 gcloud 组件:
    gcloud components update
  13. 准备开发环境。

    转到 Java 设置指南

准备应用

  1. 在终端窗口中,将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/getting-started-java.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  2. 切换到包含后台处理示例代码的目录:

    cd getting-started-java/background

了解应用

Web 应用具有两个主要组件:

  • 用于处理 Web 请求的 Java HTTP 服务器。该服务器有以下两个端点:
    • /translate
      • GET(使用网络浏览器):显示用户提交的 10 个最近处理的翻译请求。
      • POST(带 Pub/Sub 订阅):使用 Cloud Translation API 处理翻译请求,并将结果存储在 Firestore 中。
    • /create:用于提交新翻译请求的表单。
  • 处理由 Web 表单提交的翻译请求的服务客户端。有三个客户端一起工作:
    • Pub/Sub:当用户提交 Web 表单时,Pub/Sub 客户端将发布包含请求详细信息的消息。在本教程中创建的订阅将这些消息中继到您创建的 Cloud Run 端点,以执行翻译。
    • Translation:此客户端通过执行翻译来处理 Pub/Sub 请求。
    • Firestore:翻译完成后,此客户端将请求数据与翻译一起存储在 Firestore 中。该客户端还读取主要 /translate 端点上的最新请求。

了解 Cloud Run 代码

  • Cloud Run 应用依赖于 Firestore、Translation 和 Pub/Sub。

    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
      <version>3.0.16</version>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-translate</artifactId>
      <version>2.1.1</version>
    </dependency>
    
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-pubsub</artifactId>
      <version>1.113.7</version>
    </dependency>
  • 全局 Firestore、Translation 和 Pub/Sub 客户端已初始化,因此可以在调用之间重复使用它们。这样,您便无需针对每次调用初始化新的客户端(此操作会降低执行速度)。

    @WebListener("Creates Firestore and TranslateServlet service clients for reuse between requests.")
    public class BackgroundContextListener implements ServletContextListener {
      @Override
      public void contextDestroyed(javax.servlet.ServletContextEvent event) {}
    
      @Override
      public void contextInitialized(ServletContextEvent event) {
        String firestoreProjectId = System.getenv("FIRESTORE_CLOUD_PROJECT");
        Firestore firestore = (Firestore) event.getServletContext().getAttribute("firestore");
        if (firestore == null) {
          firestore =
              FirestoreOptions.getDefaultInstance().toBuilder()
                  .setProjectId(firestoreProjectId)
                  .build()
                  .getService();
          event.getServletContext().setAttribute("firestore", firestore);
        }
    
        Translate translate = (Translate) event.getServletContext().getAttribute("translate");
        if (translate == null) {
          translate = TranslateOptions.getDefaultInstance().getService();
          event.getServletContext().setAttribute("translate", translate);
        }
    
        String topicId = System.getenv("PUBSUB_TOPIC");
        TopicName topicName = TopicName.of(firestoreProjectId, topicId);
        Publisher publisher = (Publisher) event.getServletContext().getAttribute("publisher");
        if (publisher == null) {
          try {
            publisher = Publisher.newBuilder(topicName).build();
            event.getServletContext().setAttribute("publisher", publisher);
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }
  • 索引处理程序 (/) 从 Firestore 获取所有现有翻译,并使用以下列表填充 HTML 模板:

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
      CollectionReference translations = firestore.collection("translations");
      QuerySnapshot snapshot;
      try {
        snapshot = translations.limit(10).get().get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception retrieving documents from Firestore.", e);
      }
      List<TranslateMessage> translateMessages = Lists.newArrayList();
      List<QueryDocumentSnapshot> documents = Lists.newArrayList(snapshot.getDocuments());
      documents.sort(Comparator.comparing(DocumentSnapshot::getCreateTime));
    
      for (DocumentSnapshot document : Lists.reverse(documents)) {
        String encoded = gson.toJson(document.getData());
        TranslateMessage message = gson.fromJson(encoded, TranslateMessage.class);
        message.setData(decode(message.getData()));
        translateMessages.add(message);
      }
      req.setAttribute("messages", translateMessages);
      req.setAttribute("page", "list");
      req.getRequestDispatcher("/base.jsp").forward(req, resp);
    }
  • 通过提交 HTML 表单来请求新的翻译。在 /create 注册的请求翻译处理程序会解析表单提交,验证请求,并向 Pub/Sub 发布一条消息:

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      String text = req.getParameter("data");
      String sourceLang = req.getParameter("sourceLang");
      String targetLang = req.getParameter("targetLang");
    
      Enumeration<String> paramNames = req.getParameterNames();
      while (paramNames.hasMoreElements()) {
        String paramName = paramNames.nextElement();
        logger.warning("Param name: " + paramName + " = " + req.getParameter(paramName));
      }
    
      Publisher publisher = (Publisher) getServletContext().getAttribute("publisher");
    
      PubsubMessage pubsubMessage =
          PubsubMessage.newBuilder()
              .setData(ByteString.copyFromUtf8(text))
              .putAttributes("sourceLang", sourceLang)
              .putAttributes("targetLang", targetLang)
              .build();
    
      try {
        publisher.publish(pubsubMessage).get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception publishing message to topic.", e);
      }
    
      resp.sendRedirect("/");
    }
  • 您创建的 Pub/Sub 订阅会将这些请求转发到 Cloud Run 端点,该端点将解析 Pub/Sub 消息以获取要翻译的文本和所需的目标语言。然后,Translation API 会将字符串翻译为您选择的语言。

    String body = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    
    PubSubMessage pubsubMessage = gson.fromJson(body, PubSubMessage.class);
    TranslateMessage message = pubsubMessage.getMessage();
    
    // Use Translate service client to translate the message.
    Translate translate = (Translate) this.getServletContext().getAttribute("translate");
    message.setData(decode(message.getData()));
    Translation translation =
        translate.translate(
            message.getData(),
            Translate.TranslateOption.sourceLanguage(message.getAttributes().getSourceLang()),
            Translate.TranslateOption.targetLanguage(message.getAttributes().getTargetLang()));
  • 该应用将翻译数据存储在它在 Firestore 中创建的新文档中。

    // Use Firestore service client to store the translation in Firestore.
    Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
    
    CollectionReference translations = firestore.collection("translations");
    
    ApiFuture<WriteResult> setFuture = translations.document().set(message, SetOptions.merge());
    
    setFuture.get();
    resp.getWriter().write(translation.getTranslatedText());

部署 Cloud Run 应用

  1. 选择 Pub/Sub 主题名称,并使用 uuidgen 或在线 UUID 生成器(如 uuidgenerator.net)生成 Pub/Sub 验证令牌。此令牌将确保 Cloud Run 端点仅接受来自您创建的 Pub/Sub 订阅的请求。

    export PUBSUB_TOPIC=background-translate
    export PUBSUB_VERIFICATION_TOKEN=your-verification-token
  2. 创建 Pub/Sub 主题:

     gcloud pubsub topics create $PUBSUB_TOPIC
    
    • pom.xml 文件中的 MY_PROJECT 替换为您的 Cloud 项目 ID。
  3. 使用 Jib Maven 插件构建代码的映像并将其部署到 GCR(映像存储区)。

     mvn clean package jib:build
    
  4. 将应用部署到 Cloud Run:

    gcloud run deploy background --image gcr.io/MY_PROJECT/background \
          --platform managed --region us-central1 --memory 512M \
          --update-env-vars PUBSUB_TOPIC=$PUBSUB_TOPIC,PUBSUB_VERIFICATION_TOKEN=$PUBSUB_VERIFICATION_TOKEN

    其中 MY_PROJECT 是您创建的 Cloud 项目的名称。此命令输出您的 Pub/Sub 订阅将转换请求推送到的端点。记下该端点,因为您将需要它来创建 Pub/Sub 订阅,并且您将在浏览器中访问该端点以请求新翻译。

测试应用

部署 Cloud Run 服务后,请尝试请求翻译。

  1. 要在浏览器中查看应用,请转到先前创建的 Cloud Run 端点。

    您将看到一个页面,其中包含一个空翻译列表和一个用于请求新翻译的表单。

  2. 点击 + 请求翻译,填写请求表单,然后点击提交

  3. 提交会自动将您带回到 /translate 路径,但是新翻译可能尚未出现。要刷新页面,请点击刷新 。翻译列表中有一个新行。如果您未看到翻译,请再等待几秒钟,然后重试。如果您仍未看到翻译,请参阅下一部分,了解如何调试应用。

调试应用

如果您无法连接到 Cloud Run 服务或看不到新的翻译,请检查以下内容:

  • 检查 gcloud run deploy 命令是否已成功完成,并且未输出任何错误。如果有错误(如 message=Build failed),请修正这些错误,然后重试运行。

  • 查看日志以了解错误:

    1. 在 Google Cloud Console 中,转到 Cloud Run 页面。

      转到 Cloud Run 页面

    2. 点击服务名称 background

    3. 点击日志

清理

删除项目

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除 Cloud Run 服务。

  • 删除您在本教程中创建的 Cloud Run 服务:

    gcloud run services delete --region=$region background

后续步骤