后台处理

许多应用都需要在 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 帐号。

    如果您还没有 Google 帐号,请注册新帐号

  2. 在 Cloud Console 的项目选择器页面上,选择或创建 Cloud 项目。

    转到项目选择器页面

  3. 确保您的 Google Cloud 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

  4. 启用 Firestore, Pub/Sub, and Cloud Translation API。

    启用 API

  5. 安装并初始化 Cloud SDK
  6. 更新 gcloud 组件:
    gcloud components update
  7. 准备开发环境。

    转到 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>1.33.0</version>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-translate</artifactId>
      <version>1.94.5</version>
    </dependency>
    
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-pubsub</artifactId>
      <version>1.105.1</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);
        }
    
        Publisher publisher = (Publisher) event.getServletContext().getAttribute("publisher");
        if (publisher == null) {
          try {
            String topicId = System.getenv("PUBSUB_TOPIC");
            publisher =
                Publisher.newBuilder(
                        ProjectTopicName.newBuilder()
                            .setProject(firestoreProjectId)
                            .setTopic(topicId)
                            .build())
                    .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. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除 Cloud Run 服务。

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

    gcloud run services delete --region=$region background

后续步骤