Build a Kotlin Spring Application with Google Cloud Platform

1. Introduction

Spring Framework 5.0 added dedicated Kotlin support making it easy for Kotlin developers to use Spring. Consequently, these changes meant that the Google Cloud integrations provided by Spring Cloud GCP also work seamlessly in Kotlin. In this codelab you will see how easy it is to begin using Google Cloud services in your Kotlin applications!

This codelab walks through setting up a simple registration application in Kotlin which demonstrates using GCP services including: Cloud Pub/Sub and Cloud SQL.

What you'll build

In this codelab, you will setup a Kotlin Spring Boot application which accepts registrant information, publishes this to a Cloud Pub/Sub topic, and persists this to a Cloud MySQL database.

What you'll learn

How to integrate with Google Cloud services in your Kotlin Spring application.

What you'll need

  • A Google Cloud Platform Project
  • A Browser, such Chrome or Firefox

How will you use use this tutorial?

Read it through only Read it and complete the exercises

How would you rate your experience with building HTML/CSS web apps?

Novice Intermediate Proficient

How would you rate your experience with using Google Cloud Platform services?

Novice Intermediate Proficient

2. Setup and Requirements

Self-paced environment setup

  1. Sign in to Cloud Console and create a new project or reuse an existing one. (If you don't already have a Gmail or G Suite account, you must create one.)

dMbN6g9RawQj_VXCSYpdYncY-DbaRzr2GbnwoV7jFf1u3avxJtmGPmKpMYgiaMH-qu80a_NJ9p2IIXFppYk8x3wyymZXavjglNLJJhuXieCem56H30hwXtd8PvXGpXJO9gEUDu3cZw

ci9Oe6PgnbNuSYlMyvbXF1JdQyiHoEgnhl4PlV_MFagm2ppzhueRkqX4eLjJllZco_2zCp0V0bpTupUSKji9KkQyWqj11pqit1K1faS1V6aFxLGQdkuzGp4rsQTan7F01iePL5DtqQ

8-tA_Lheyo8SscAVKrGii2coplQp2_D1Iosb2ViABY0UUO1A8cimXUu6Wf1R9zJIRExL5OB2j946aIiFtyKTzxDcNnuznmR45vZ2HMoK3o67jxuoUJCAnqvEX6NgPGFjCVNgASc-lg

Remember the project ID, a unique name across all Google Cloud projects (the name above has already been taken and will not work for you, sorry!). It will be referred to later in this codelab as PROJECT_ID.

  1. Next, you'll need to enable billing in Cloud Console in order to use Google Cloud resources.

Running through this codelab shouldn't cost much, if anything at all. Be sure to to follow any instructions in the "Cleaning up" section which advises you how to shut down resources so you don't incur billing beyond this tutorial. New users of Google Cloud are eligible for the $300USD Free Trial program.

Google Cloud Shell

While Google Cloud can be operated remotely from your laptop, in this codelab we will be using Google Cloud Shell, a command line environment running in the Cloud.

Activate Cloud Shell

  1. From the Cloud Console, click Activate Cloud Shell H7JlbhKGHITmsxhQIcLwoe5HXZMhDlYue4K-SPszMxUxDjIeWfOHBfxDHYpmLQTzUmQ7Xx8o6OJUlANnQF0iBuUyfp1RzVad_4nCa0Zz5LtwBlUZFXFCWFrmrWZLqg1MkZz2LdgUDQ.

zlNW0HehB_AFW1qZ4AyebSQUdWm95n7TbnOr7UVm3j9dFcg6oWApJRlC0jnU1Mvb-IQp-trP1Px8xKNwt6o3pP6fyih947sEhOFI4IRF0W7WZk6hFqZDUGXQQXrw21GuMm2ecHrbzQ

If you've never started Cloud Shell before, you'll be presented with an intermediate screen (below the fold) describing what it is. If that's the case, click Continue (and you won't ever see it again). Here's what that one-time screen looks like:

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

It should only take a few moments to provision and connect to Cloud Shell.

pTv5mEKzWMWp5VBrg2eGcuRPv9dLInPToS-mohlrqDASyYGWnZ_SwE-MzOWHe76ZdCSmw0kgWogSJv27lrQE8pvA5OD6P1I47nz8vrAdK7yR1NseZKJvcxAZrPb8wRxoqyTpD-gbhA

This virtual machine is loaded with all the development tools you'll need. It offers a persistent 5GB home directory and runs in Google Cloud, greatly enhancing network performance and authentication. Much, if not all, of your work in this codelab can be done with simply a browser or your Chromebook.

Once connected to Cloud Shell, you should see that you are already authenticated and that the project is already set to your project ID.

  1. Run the following command in Cloud Shell to confirm that you are authenticated:
gcloud auth list

Command output

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

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

Command output

[core]
project = <PROJECT_ID>

If it is not, you can set it with this command:

gcloud config set project <PROJECT_ID>

Command output

Updated property [core/project].

3. Provision Pub/Sub resources

First, we'll need to setup a Cloud Pub/Sub topic and subscription. In this application, we will be publishing registration info to a Pub/Sub topic; the information is then read from this topic and persisted to a database.

In this tutorial, we will rely on the Cloud Shell to provision our resources. Note that one may also configure Pub/Sub resources through the Cloud Pub/Sub section in Google Cloud Console.

In your Cloud Shell terminal, first enable the Pub/Sub API.

$ gcloud services enable pubsub.googleapis.com

Next, we will create a Pub/Sub topic named registrations for this application. The registration info submitted through the application will be published to this topic.

$ gcloud pubsub topics create registrations

Finally, create a subscription for the topic. A Pub/Sub subscription enables you to receive messages from a topic.

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

You have now completed creating a Cloud Pub/Sub topic and subscription for your application.

4. Create a Cloud SQL (MySQL) Instance and Database

For our sample application, we also need to setup a database instance to hold the registrant information. This step will also rely on the Cloud Shell terminal to provision Cloud SQL resources. Note that you may view and configure your Cloud SQL instances through the Google Cloud Console as well.

First, enable the Cloud SQL Admin API.

$ gcloud services enable sqladmin.googleapis.com

Next, we will provision a Cloud SQL (MySQL) instance. This command may take some time.

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

After you successfully created your Cloud SQL instance, create a new database in your instance called registrants.

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

You have now completed the Cloud SQL instance and database setup for your application.

5. Initialize a Spring Boot Application

We are now ready to begin writing the application. The next steps will continue using the Cloud Shell described in the setup steps.

First, we will use Initializr to generate the scaffolding code for the project. In your Cloud Shell window, run:

$ 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

This command generates an initial Maven project setup as well as scaffolding code for your application in the registrations-codelab/ directory. The following sections describe code edits necessary to produce a working application.

Cloud Shell Code Editor

The easiest way to begin modifying and viewing code in the Cloud Shell environment is to use the built-in Cloud Shell Code Editor.

Once you have opened a Cloud Shell instance, click on the Pencil icon to open the code editor. The editor should allow you to directly modify the project files produced by Initialzr.

cce293b40119c37b.png

6. Database Configuration

First, configure your application so that it can connect to the Cloud MySQL database that you set up. The Spring Cloud GCP libraries offer a Cloud MySQL starter which provides the necessary dependencies for connecting to a Cloud MySQL instance.

Add the spring-cloud-gcp-starter-sql-mysql dependency to the project 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>

In addition, you need to modify the application.properties configuration file to describe your database configuration. Copy the following properties into your application.properties file.

Find the instance connection name to your database:

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

The output of this will be used in application.properties file to configure the connection information.

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

The only property you must modify is the instance connection name. This value must be formatted as a colon-separated value with the form: YOUR_GCP_PROJECT_ID:REGION:DATABASE_INSTANCE_NAME.

7. Creating the Static Content

First, we will create the frontend for our application. The application should have a form allowing someone to register individuals and also a view which displays all successful registrants.

For the home page, create an index.html containing the registration form.

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>

Next, we will create a Thymeleaf template named registrants.html for displaying the registered users. Thymeleaf is a templating framework which we use to construct and serve dynamically-created HTML. You will see that the template looks like HTML, except that it has some additional markdown elements to handle dynamic content. This template accepts a single parameter called personsList which contains all registrants who were registered through the application.

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>

At this point, you can verify that the static content is being served.

Build and run the app using Maven:

$ ./mvnw spring-boot:run

Click on the preview button in Cloud Shell window and verify that you see the home page being rendered. None of the functionality on the UI will work though because we are missing a web controller. This will be added in the next step.

5e38bb0d0e93002e.png

After previewing the application, press CTRL+C to terminate the application.

8. Sending the Registrants to a Pub/Sub Topic

In this step, we will implement the feature where registrants submitted through the webform will be published to a Cloud Pub/Sub topic.

Add the Data Classes

First, we will be creating some Kotlin data classes; these will be our JPA entities and also act as our intermediate representation of the registrants submitted through the form.

In the demo package, add two new files: a Person class and a Spring Data PersonRepository. These two classes will allow us to easily store and retrieve registration entries from our MySQL database using Spring Data JPA.

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>

Add the Web Controller

Next, we will create a Controller class which processes registrants from the form and sends the information to the Cloud Pub/Sub topic you created earlier. This controller creates two endpoints:

  • /registerPerson: The POST endpoint in which registrant info is submitted and then sent to the Pub/Sub topic. In the registerPerson(..) function, registrant info is sent to the Pub/Sub topic using PubSubTemplate, a convenience class from the Spring Cloud GCP Pub/Sub integrations which minimizes the boilerplate code needed to begin interacting with Cloud Pub/Sub.
  • /registrants: Displays all the registrants successfully registered in the database. This information is retrieved from the MySQL instance using the Spring Data repository that we created in the previous step.

Create the following Controller class in the demo package:

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))
  }
}

The controller reads the registrant information submitted through the web form and then publishes the information to the Pub/Sub topic.

Adding the JSON Object Mapper Bean

You might have noticed in the Controller that we publish a Person object to the Pub/Sub topic and not a String. This is possible because we take advantage of Spring Cloud GCP support for custom JSON payloads to be sent to topics - the libraries allow you to serialize objects to JSON, send JSON payloads to a topic, and deserialize the payload when it is received.

In order to take advantage of this feature, we must add an ObjectMapper bean to your application context. This ObjectMapper bean will be used to serialize objects to and from JSON when your application sends and receives messages. In the DemoApplication.kt class, add the 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)
}

At this point, you can try running the application again by running:

$ ./mvnw spring-boot:run

From the web form on the main page, the application will now send the information to the Pub/Sub topic that you created. However, it's still not doing anything useful yet because we still need to read from that Pub/Sub topic! This is accomplished in the next step.

9. Reading Registrants from the Pub/Sub Topic

In the final step, we will be processing registrant info from the Pub/Sub topic and persisting the information to the Cloud MySQL database. This will complete the application, allowing you to submit new registrants through the form and view all registered users through the /registrants endpoint.

This application will take advantage of Spring Integration, which offers many convenient abstractions for dealing with messaging. We will add a PubSubInboundChannelAdapter to enable us to read messages from the Pub/Sub topic and put them on the pubsubInputChannel for further processing. We will then configure the messageReceiver function using @ServiceActivator to be invoked with the messages arriving on the 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)
}

At this point, you have completed the setup for the application. To verify that the app works properly, run:

$ ./mvnw spring-boot:run

Click on the Preview button again and try registering a user by filling out the form and submitting.

e0d0b0f0c94120c2.png

Click on the Registered People link to verify that the new registrant appears in the table.

ab3b980423d0c51.png

Congratulations, you are now done! Terminate the application by pressing CTRL+C in the terminal window.

10. Cleanup

To clean up your environment, you need to delete the Pub/Sub topic and Cloud MySQL instance you created.

Deleting the Cloud MySQL Instance

$ gcloud sql instances delete codelab-instance

Deleting the Pub/Sub Resources

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

11. Congratulations!

You have now completed writing a Spring Kotlin application which integrates with Cloud Pub/Sub and Cloud SQL (MySQL).

Learn More

License

This work is licensed under a Creative Commons Attribution 2.0 Generic License.