使用 Google Cloud Platform 构建 Kotlin Spring 应用

1. 简介

Spring Framework 5.0 添加了专属 Kotlin 支持,让 Kotlin 开发者能够轻松使用 Spring。因此,这些变化意味着 Spring Cloud GCP 提供的 Google Cloud 集成在 Kotlin 中也可以无缝运行。在此 Codelab 中,您将了解如何轻松在 Kotlin 应用中使用 Google Cloud 服务!

此 Codelab 将逐步介绍如何在 Kotlin 中设置简单的注册应用,该应用演示了如何使用 GCP 服务,包括 Cloud Pub/SubCloud SQL

构建内容

在此 Codelab 中,您将设置一个 Kotlin Spring Boot 应用,该应用接受注册者信息,将其发布到 Cloud Pub/Sub 主题,并将其持久保留在 Cloud MySQL 数据库中。

学习内容

如何在 Kotlin Spring 应用中与 Google Cloud 服务集成。

所需条件

  • 一个 Google Cloud Platform 项目
  • 浏览器,例如 Chrome 或 Firefox

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价自己在构建 HTML/CSS Web 应用方面的经验水平?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud Platform 服务方面的经验水平?

<ph type="x-smartling-placeholder"></ph> 新手 中级 熟练

2. 设置和要求

自定进度的环境设置

  1. 登录 Cloud 控制台,然后创建一个新项目或重复使用现有项目。 (如果您还没有 Gmail 或 G Suite 账号,则必须创建一个。)

dMbN6g9RawQj_VXCSYpdYncY-DbaRzr2GbnwoV7jFf1u3avxJtmGPmKpMYgiaMH-qu80a_NJ9p2IIXFppYk8x3wyymZXavjglNLJJhuXieCem56H30hwXtd8PvXGpXJO9gEUDu3cZw

ci9Oe6PgnbNuSYlMyvbXF1JdQyiHoEgnhl4PlV_MFagm2ppzhueRkqX4eLjJllZco_2zCp0V0bpTupUSKji9KkQyWqj11pqit1K1faS1V6aFxLGQdkuzGp4rsQTan7F01iePL5DtqQ

8-tA_Lheyo8SscAVKrGii2coplQp2_D1Iosb2ViABY0UUO1A8cimXUu6Wf1R9zJIRExL5OB2j946aIiFtyKTzxDcNnuznmR45vZ2HMoK3o67jxuoUJCAnqvEX6NgPGFjCVNgASc-lg

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID

  1. 接下来,您需要在 Cloud 控制台中启用结算功能,才能使用 Google Cloud 资源。

运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照“清理”部分部分,其中会指导您如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

Google Cloud Shell

虽然 Google Cloud 可以通过笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud ShellH7JlbhKGHITmsxhQIcLwoe5HXZMhDlYue4K-SPszMxUxDjIeWfOHBfxDHYpmLQTzUmQ7Xx8o6OJUlANnQF0iBuUyfp1RzVad_4nCa0Zz5LtwBlUZFXFCWFrmrWZLqg1MkZz2LdgUDQ

zlNW0HehB_AFW1qZ4AyebSQUdWm95n7TbnOr7UVm3j9dFcg6oWApJRlC0jnU1Mvb-IQp-trP1Px8xKNwt6o3pP6fyih947sEhOFI4IRF0W7WZk6hFqZDUGXQQXrw21GuMm2ecHrbzQ

如果您以前从未启动过 Cloud Shell,将看到一个中间屏幕(在折叠下面),描述它是什么。如果是这种情况,请点击继续(您将永远不会再看到它)。一次性屏幕如下所示:

kEPbNAo_w5C_pi9QvhFwWwky1cX8hr_xEMGWySNIoMCdi-Djx9AQRqWn-__DmEpC7vKgUtl-feTcv-wBxJ8NwzzAp7mY65-fi2LJo4twUoewT1SUjd6Y3h81RG3rKIkqhoVlFR-G7w

预配和连接到 Cloud Shell 只需花几分钟时间。

pTv5mEKzWMWp5VBrg2eGcuRPv9dLInPToS-mohlrqDASyYGWnZ_SwE-MzOWHe76ZdCSmw0kgWogSJv27lrQE8pvA5OD6P1I47nz8vrAdK7yR1NseZKJvcxAZrPb8wRxoqyTpD-gbhA

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:

  1. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list

命令输出

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果不是上述结果,您可以使用以下命令进行设置:

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

3. 预配 Pub/Sub 资源

首先,我们需要设置 Cloud Pub/Sub 主题和订阅。在此应用中,我们会将注册信息发布到 Pub/Sub 主题;然后,系统会从此主题中读取信息并将其持久存储到数据库中。

在本教程中,我们将依靠 Cloud Shell 来预配资源。请注意,您还可以通过 Google Cloud 控制台中的 Cloud Pub/Sub 部分配置 Pub/Sub 资源。

在 Cloud Shell 终端中,首先启用 Pub/Sub API。

$ gcloud services enable pubsub.googleapis.com

接下来,我们将为此应用创建一个名为 registrations 的 Pub/Sub 主题。通过申请提交的注册信息将发布到此主题。

$ gcloud pubsub topics create registrations

最后,为该主题创建订阅。借助 Pub/Sub 订阅,您可以从主题接收消息。

$ gcloud pubsub subscriptions create registrations-sub --topic=registrations

现在,您已经为应用创建了 Cloud Pub/Sub 主题和订阅。

4. 创建 Cloud SQL (MySQL) 实例和数据库

对于我们的示例应用,我们还需要设置一个数据库实例来保存注册者信息。此步骤还将依赖于 Cloud Shell 终端来预配 Cloud SQL 资源。请注意,您也可以通过 Google Cloud 控制台查看和配置 Cloud SQL 实例。

首先,启用 Cloud SQL Admin API。

$ gcloud services enable sqladmin.googleapis.com

接下来,我们将预配一个 Cloud SQL (MySQL) 实例。此命令可能需要一些时间。

$ gcloud sql instances create codelab-instance --region=us-east1

成功创建 Cloud SQL 实例后,在实例中创建一个名为 registrants 的新数据库。

$ gcloud sql databases create registrants --instance codelab-instance

现在,您已完成应用的 Cloud SQL 实例和数据库设置。

5. 初始化 Spring Boot 应用

现在,我们可以开始编写应用了。后续步骤将继续使用设置步骤中描述的 Cloud Shell。

首先,我们将使用 Initializr 为项目生成基架代码。在 Cloud Shell 窗口中,运行以下命令:

$ cd ~
$ curl https://start.spring.io/starter.tgz \
  -d language=kotlin \
  -d bootVersion=2.4.0 \
  -d dependencies=web,data-jpa,integration,cloud-gcp-pubsub,thymeleaf \
  -d baseDir=registrations-codelab | tar -xzvf -
$ cd registrations-codelab

此命令会在 registrations-codelab/ 目录中为您的应用生成初始 Maven 项目设置以及基架代码。以下部分介绍了生成正常运行应用所需的代码编辑。

Cloud Shell 代码编辑器

若要开始在 Cloud Shell 环境中修改和查看代码,最简单的方法是使用内置的 Cloud Shell 代码编辑器

打开 Cloud Shell 实例后,点击铅笔图标以打开代码编辑器。编辑器应允许您直接修改 Initialzr 生成的项目文件。

cce293b40119c37b.png

6. 数据库配置

首先,配置应用,使其可以连接到您设置的 Cloud MySQL 数据库。Spring Cloud GCP 库提供了一个 Cloud MySQL 入门版,它提供了连接到 Cloud MySQL 实例所需的依赖项。

spring-cloud-gcp-starter-sql-mysql 依赖项添加到项目 pom.xml 中:

registrations-codelab/pom.xml

...
<dependencies>

  ... Other dependencies above ...

  <!-- Add the MySQL starter to the list of dependencies -->
  <dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
  </dependency>
</dependencies>

此外,您还需要修改 application.properties 配置文件以描述您的数据库配置。将以下属性复制到 application.properties 文件中。

找到与数据库的实例连接名称:

$ gcloud sql instances describe codelab-instance \
  --format 'value(connectionName)'

其输出将在 application.properties 文件中用于配置连接信息。

src/main/resources/application.properties

# Modify this property using the output from the previous command line.
spring.cloud.gcp.sql.instance-connection-name=INSTANCE_CONNECTION_NAME

# Your database name
spring.cloud.gcp.sql.database-name=registrants

# So app starts despite "table already exists" errors.
spring.datasource.continue-on-error=true

# Enforces database initialization
spring.datasource.initialization-mode=always

# Cloud SQL (MySQL) only supports InnoDB, not MyISAM
spring.jpa.database-platform=org.hibernate.dialect.MySQL55Dialect
spring.jpa.hibernate.ddl-auto=create-drop

# This is used if you want to connect to a different database instance
# user other than root; not used in codelab.
# spring.datasource.username=root

# This is used to specify the password of the database user;
# not used in codelab.
# spring.datasource.password=password

您必须修改的唯一属性是实例连接名称。此值必须采用以英文冒号分隔的值格式,格式为:YOUR_GCP_PROJECT_ID:REGION:DATABASE_INSTANCE_NAME

7. 创建静态内容

首先,我们将为应用创建前端。申请表应包含可供个人注册者的表单,以及显示所有成功注册者的视图。

对于首页,创建一个包含注册表单的 index.html

src/main/resources/static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Registration Sample Application</title>
</head>
<body>

<h1>Registration</h1>

<div>
  <nav>
    <a href="/">Home</a><br>
    <a href="/registrants">Registered People</a><br>
  </nav>

  <p>
    This is a demo registration application which sends user information to a Pub/Sub topic and
    persists it into a MySQL database.
  </p>

  <h2>Register Person</h2>
  <div>
    <form action="/registerPerson" method="post">
      First Name: <input type="text" name="firstName" />
      Last Name: <input type="text" name="lastName" />
      Email: <input type="text" name="email" />
      <input type="submit" value="Submit"/>
    </form>
  </div>
</div>

</body>
</html>

接下来,我们将创建一个名为 registrants.htmlThymeleaf 模板,用于显示注册用户。Thymeleaf 是一个模板框架,我们用它来构建和提供动态创建的 HTML。您会发现该模板看起来很像 HTML,只不过它有一些额外的 Markdown 元素来处理动态内容。此模板接受一个名为 personsList 的参数,该参数中包含已通过申请注册的所有注册者。

src/main/resources/templates/registrants.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Registrants List</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h1>Registrants List</h1>
<p>
  This page displays all the people who were registered through the Pub/Sub topic.
  All results are retrieved from the MySQL database.
</p>
<table border="1">
  <tr>
    <th>First Name</th>
    <th>Last Name</th>
    <th>Email</th>
  </tr>
  <tr th:each="person : ${personsList}">
    <td>[[${person.firstName}]]</td>
    <td>[[${person.lastName}]]</td>
    <td>[[${person.email}]]</td>
  </tr>
</table>

</body>
</html>

此时,您可以验证是否正在传送静态内容。

使用 Maven 构建并运行应用:

$ ./mvnw spring-boot:run

点击 Cloud Shell 窗口中的预览按钮,验证您是否看到呈现的首页。但是,由于缺少网页控制器,此界面上的所有功能都将无法正常运行。这些信息将在下一步中添加。

5e38bb0d0e93002e

预览应用后,按 CTRL+C 终止应用。

8. 将注册者发送到 Pub/Sub 主题

在此步骤中,我们将实现一项功能,即通过网络表单提交的注册者将被发布到 Cloud Pub/Sub 主题。

添加数据类

首先,我们将创建一些 Kotlin 数据类;这些机构将成为我们的 JPA 实体,并充当我们的中间代表通过此表单提交的注册者。

在演示软件包中,添加两个新文件:Person 类和 Spring Data PersonRepository。这两个类让我们可以使用 Spring Data JPA 轻松地存储和检索 MySQL 数据库中的注册条目。

src/main/kotlin/com/example/demo/Person.kt

package com.example.demo

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
data class Person(
    val firstName: String,
    val lastName: String,
    val email: String,
    @Id @GeneratedValue
    var id: Long? = 0)

src/main/kotlin/com/example/demo/PersonRepository.kt

package com.example.demo

import org.springframework.data.repository.CrudRepository

interface PersonRepository : CrudRepository<Person, Long>

添加 Web 控制器

接下来,我们将创建一个 Controller 类,该类用于处理来自表单的注册者,并将信息发送到您之前创建的 Cloud Pub/Sub 主题。此控制器会创建两个端点:

  • /registerPerson:POST 端点,注册者信息会提交到该端点,然后发送到 Pub/Sub 主题。在 registerPerson(..) 函数中,注册者信息使用 PubSubTemplate 发送到 Pub/Sub 主题,这是 Spring Cloud GCP Pub/Sub 集成中的一种便捷类,可最大限度地减少开始与 Cloud Pub/Sub 交互所需的样板代码。
  • /registrants:显示数据库中成功注册的所有注册者。此信息是使用我们在上一步中创建的 Spring Data 仓库从 MySQL 实例中检索到的。

在演示软件包中创建以下 Controller 类:

src/main/kotlin/com/example/demo/Controller.kt

package com.example.demo

import com.google.cloud.spring.pubsub.core.PubSubTemplate
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.view.RedirectView

@RestController
class Controller(val pubSubTemplate: PubSubTemplate, val personRepository: PersonRepository) {
  
  // The Pub/Sub topic name created earlier.
  val REGISTRATION_TOPIC = "registrations"

  @PostMapping("/registerPerson")
  fun registerPerson(
    @RequestParam("firstName") firstName: String,
    @RequestParam("lastName") lastName: String,
    @RequestParam("email") email: String): RedirectView {

    pubSubTemplate.publish(
        REGISTRATION_TOPIC,
        Person(firstName, lastName, email))
    return RedirectView("/")
  }

  @GetMapping("/registrants")
  fun getRegistrants(): ModelAndView {
    val personsList = personRepository.findAll().toList()
    return ModelAndView("registrants", mapOf("personsList" to personsList))
  }
}

控制器会读取通过网络表单提交的注册者信息,然后将信息发布到 Pub/Sub 主题。

添加 JSON 对象映射器 Bean

您可能已经注意到,我们在 Controller 中将 Person 对象发布到了 Pub/Sub 主题,而不是一个字符串。之所以能够实现这一点,是因为我们利用了 Spring Cloud GCP 支持将自定义 JSON 载荷发送到主题的功能。借助这些库,您可以将对象序列化为 JSON,将 JSON 载荷发送到主题,并在收到载荷时将其反序列化。

为了利用此功能,我们必须向应用上下文添加一个 ObjectMapper Bean。当应用发送和接收消息时,此 ObjectMapper Bean 将用于在 JSON 中对对象进行序列化。在 DemoApplication.kt 类中,添加 JacksonPubSubMessageConverter Spring bean:

src/main/kotlin/com/example/demo/DemoApplication.kt

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

// new imports to add
import org.springframework.context.annotation.Bean
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.cloud.spring.pubsub.support.converter.JacksonPubSubMessageConverter

@SpringBootApplication
class DemoApplication {
  // This bean enables serialization/deserialization of
  // Java objects to JSON for Pub/Sub payloads
  @Bean
  fun jacksonPubSubMessageConverter(objectMapper: ObjectMapper) = 
      JacksonPubSubMessageConverter(objectMapper)
}

fun main(args: Array<String>) {
        runApplication<DemoApplication>(*args)
}

此时,您可以通过运行以下命令再次尝试运行应用:

$ ./mvnw spring-boot:run

现在,应用会通过主页面上的网络表单将信息发送到您创建的 Pub/Sub 主题。但是,它仍然没有任何用处,因为我们仍需从该 Pub/Sub 主题中读取数据!这会在下一步中实现。

9. 读取 Pub/Sub 主题中的注册者

在最后一步中,我们会处理 Pub/Sub 主题中的注册者信息,并将信息保存到 Cloud MySQL 数据库中。此操作将完成申请,这样您就可以通过表单提交新的注册者,并通过 /registrants 端点查看所有注册用户。

此应用将利用 Spring Integration,为处理消息传递提供许多便捷的抽象。我们将添加一个 PubSubInboundChannelAdapter,使我们能够读取 Pub/Sub 主题中的消息,并将其放在 pubsubInputChannel 中以供进一步处理。然后,我们将使用 @ServiceActivator 配置 messageReceiver 函数,使其通过到达 pubsubInputChannel 的消息进行调用。

src/main/kotlin/com/example/demo/DemoApplication.kt

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

import org.springframework.context.annotation.Bean
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.cloud.gcp.pubsub.support.converter.JacksonPubSubMessageConverter

// new imports to add
import com.google.cloud.spring.pubsub.core.PubSubTemplate
import com.google.cloud.spring.pubsub.integration.AckMode
import com.google.cloud.spring.pubsub.integration.inbound.PubSubInboundChannelAdapter
import com.google.cloud.spring.pubsub.support.BasicAcknowledgeablePubsubMessage
import com.google.cloud.spring.pubsub.support.GcpPubSubHeaders
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.integration.annotation.ServiceActivator
import org.springframework.integration.channel.DirectChannel
import org.springframework.messaging.MessageChannel
import org.springframework.messaging.handler.annotation.Header

@SpringBootApplication
class DemoApplication {

  private val REGISTRANT_SUBSCRIPTION = "registrations-sub"

  @Autowired
  private lateinit var personRepository: PersonRepository

  // New Spring Beans to add
  @Bean
  fun pubsubInputChannel() = DirectChannel()

  @Bean
  fun messageChannelAdapter(
      @Qualifier("pubsubInputChannel") inputChannel: MessageChannel,
      pubSubTemplate: PubSubTemplate): PubSubInboundChannelAdapter {

    val adapter = PubSubInboundChannelAdapter(
        pubSubTemplate, REGISTRANT_SUBSCRIPTION)
    adapter.outputChannel = inputChannel
    adapter.ackMode = AckMode.MANUAL
    adapter.payloadType = Person::class.java
    return adapter
  }

  @ServiceActivator(inputChannel = "pubsubInputChannel")
  fun messageReceiver(
      payload: Person,
      @Header(GcpPubSubHeaders.ORIGINAL_MESSAGE) message: BasicAcknowledgeablePubsubMessage) {
    personRepository.save(payload)
    print("Message arrived! Payload: $payload")
    message.ack()
  }

  // ObjectMapper bean from previous step
  @Bean
  fun jacksonPubSubMessageConverter(objectMapper: ObjectMapper) = JacksonPubSubMessageConverter(objectMapper)
}

fun main(args: Array<String>) {
        runApplication<DemoApplication>(*args)
}

此时,您已完成应用的设置流程。如需验证应用是否正常运行,请运行以下命令:

$ ./mvnw spring-boot:run

再次点击预览按钮,然后填写表单并提交,尝试注册用户。

e0d0b0f0c94120c2.png

点击已注册的用户链接,验证新的注册者是否显示在表格中。

ab3b980423d0c51.png

恭喜,大功告成!在终端窗口中按 CTRL+C 终止应用。

10. 清理

如需清理环境,您需要删除您创建的 Pub/Sub 主题和 Cloud MySQL 实例。

删除 Cloud MySQL 实例

$ gcloud sql instances delete codelab-instance

删除 Pub/Sub 资源

$ gcloud pubsub subscriptions delete registrations-sub
$ gcloud pubsub topics delete registrations

11. 恭喜!

您现在已经编写了一个与 Cloud Pub/Sub 和 Cloud SQL (MySQL) 集成的 Spring Kotlin 应用。

了解详情

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。