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

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

Self-paced environment setup

If you don't already have a Google Account (Gmail or Google Apps), you must create one. Sign-in to Google Cloud Platform console (console.cloud.google.com) and create a new project:

Screenshot from 2016-02-10 12:45:26.png

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.

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

Running through this codelab shouldn't cost you more than a few dollars, but it could be more if you decide to use more resources or if you leave them running (see "cleanup" section at the end of this document).

New users of Google Cloud Platform are eligible for a $300 free trial.

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 Google Cloud Shell

From the GCP Console click the Cloud Shell icon on the top right toolbar:

Then click "Start Cloud Shell":

It should only take a few moments to provision and connect to the environment:

This virtual machine is loaded with all the development tools you'll need. It offers a persistent 5GB home directory, and runs on the Google Cloud, greatly enhancing network performance and authentication. Much, if not all, of your work in this lab can be done with simply a browser or your Google 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.

Run the following command in Cloud Shell to confirm that you are authenticated:

gcloud auth list

Command output

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
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].

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.

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.

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.1.8.RELEASE \
  -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.

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>org.springframework.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.

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.

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

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:

Create the following Controller class in the demo package:

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

package com.example.demo

import org.springframework.cloud.gcp.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 org.springframework.cloud.gcp.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.

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 org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cloud.gcp.pubsub.core.PubSubTemplate
import org.springframework.cloud.gcp.pubsub.integration.AckMode
import org.springframework.cloud.gcp.pubsub.integration.inbound.PubSubInboundChannelAdapter
import org.springframework.cloud.gcp.pubsub.support.BasicAcknowledgeablePubsubMessage
import org.springframework.cloud.gcp.pubsub.support.GcpPubSubHeaders
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.

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

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

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

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.