I have build a great number of Spring Boot applications, very often ones with a web interface. Spring Boot offers a lot of flexibility and configuration options. This can come at the cost of a rather large deployment artifact (usually a JAR file) and noticeable startup times. For smaller projects you might not want to pay that price.

In this blog post we will look at an alternative approach for a very small web application. We use Spark, a Jetty based microframework. Spark relies a lot on lambdas and static methods providing a concise and easy to read setup for a web application.

To make it not to trivial it will also include some basic database access. The application will be written in Kotlin, my go-to language for the JVM.

All code can be found on our GitHub account.

Setting up the Project

Let’s start with a Gradle file.

group 'de.techdev.example'
version '1.0'

buildscript {
    ext.kotlin_version = '1.1.2-2'

    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'

repositories {
    mavenCentral()
}

task fatJar(type: Jar) {
    baseName = project.name + '-fat'
    from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
    manifest {
        attributes 'Main-Class': 'de.techdev.example.SparkExampleKt'
    }
}

build.dependsOn fatJar

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
    compile 'com.sparkjava:spark-core:2.5.5'
    compile 'org.slf4j:slf4j-simple:1.7.25'
}

In line 16 we enable the Kotlin plugin so we can actually compile Kotlin.

In line 22 we introduce a new Gradle task to build a fat JAR file that contains all the dependencies. It iterates over all runtime dependencies and unzips them if needed. The zipTree function does that.

Line 27 is very interesting. We will see later why we need to use the strange class name with the appended Kt here.

The dependencies obviously include the Kotlin standard library, Spark and a simple logging binding.

Adding a First REST Endpoint

Now we can add our first web endpoint! This file can be found under src/main/kotlin/de/techdev/example/SparkExample.kt.

package de.techdev.example

import spark.Spark.get

fun main(args: Array<String>) {
    get("/",  { _, _ -> "hello" })
}

The main function can be started in your IDE. If you then perform a simple curl http://localhost:4567/ you should see the hello text! Notice the extremely fast startup time (303ms on my laptop). Since we neither use the request or response parameters of the lambda we give them the empty _ name.

Why Use SparkExampleKt as the Main Class?

Because we use the „free“ main function (not inside a class in Kotlin) we need to write de.techdev.example.SparkExample.kt as the main class into the manifest with Gradle. In the JVM everything must belong to a class. Kotlin allows functions outside of a class but will generate bytecode where these functions are put into a class that has the name of the file where the function is inside! It also appends Kt to the name.

This allows the following to be interoperable with Java.

// File Foo.kt

class Foo

fun bar() { }

Adding a Thin JDBC Wrapper

We will now extend our REST endpoint and load some data from a database which will be rendered as JSON.

Let’s start with the database interaction. I will use H2 and plain JDBC to interact with the database. Using Kotlin this can actually be fun for small projects!

Let’s look at a wrapper class around a JDBC DataSource, in src/main/kotlin/de/techdev/example/Jdbc.kt.

package de.techdev.example

import org.h2.jdbcx.JdbcDataSource
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import javax.sql.DataSource

class Jdbc {

    private val dataSource: DataSource

    init {
        dataSource = JdbcDataSource()
        // DB_CLOSE_DELAY needed so the tables don't get removed after a connection is closed
        dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
        dataSource.user = "sa"
        dataSource.password = "sa"
    }

    /**
     * Executes a query like an INSERT or UPDATE
     */
    fun execute(sql: String) {
        callInternal(sql, {
            it.execute()
        })
    }

    /**
     * Queries the database for a list of objects.
     */
    fun <T> queryForList(sql: String, rowMapper: (rs: ResultSet) -> T): List<T> {
        return callInternal(sql, {
            val result = ArrayList<T>()
            val rs = it.executeQuery()
            while (rs.next()) {
                result.add(rowMapper.invoke(rs))
            }
            result
        })
    }

    /**
     * Creates and cleans up a prepared statement with a user provided action on the statement.
     */
    private fun <T> callInternal(sql: String, action: (pstmt: PreparedStatement) -> T): T {
        var conn: Connection? = null
        var pstmt: PreparedStatement? = null
        try {
            conn = dataSource.connection
            pstmt = conn.prepareStatement(sql)
            return action.invoke(pstmt)
        } finally {
            conn?.close()
            pstmt?.close()
        }
    }
}

Let’s go through the interesting lines.

  • Line 14: Here we create the H2 in memory datasource. It is important to set the DB_CLOSE_DELAY to -1, otherwise the database will be empty for each new connection.
  • Line 47: This internal method handles a connection and prepared statement while allowing a custom „action“ on the prepared statement. It allows to create higher level functions without to much repetition of the boilerplate.
  • Line 24: This is a method to execute an INSERT or UPDATE for example. Note the use of the automatic it variable in a lambda – it is the PreparedStatement!
  • Line 33: This methods is for a SELECT that is expected to return a list. The caller has to provide a mapper that creates the objects from the ResultSet.

To use it we add the H2 dependency to build.gradle.

dependencies {
    // other dependencies
    compile 'com.h2database:h2:1.4.195'
}

With this we can already extend our web endpoints!

Adding an Endpoint With Data From the Database

Since we use an in-memory H2 database we have to create the table structure and initial data on startup. We will use this schema and data.

CREATE TABLE person (
  id         IDENTITY PRIMARY KEY,
  first_name VARCHAR,
  last_name  VARCHAR
);

INSERT INTO person (first_name, last_name) VALUES ('Moritz', 'Schulze');
INSERT INTO person (first_name, last_name) VALUES ('Alexander', 'Hanschke');
INSERT INTO person (first_name, last_name) VALUES ('Adrian', 'Krion');

We can execute this file like this, going back to SparkExample.kt.

fun main(args: Array<String>) {
    val jdbc = Jdbc()
    createTablesAndData(jdbc)

    get("/", { _, _ -> "hello" })
}

private fun createTablesAndData(jdbc: Jdbc) {
    val initialSql = jdbc.javaClass.classLoader.getResource("initialData.sql").readText()
    jdbc.execute(initialSql)
}

We load the resource file by using the class loader and execute the contained SQL with our JDBC wrapper.

Of course now we want to read the data as well and return it as JSON. Spark does not have automatic JSON support, we have to add it ourselves. We will use Google’s Gson for this.

So we add the dependency to build.gradle.

dependencies {
    // other dependencies
    compile 'com.h2database:h2:1.4.195'
    compile 'com.google.code.gson:gson:2.8.0'
}

And now extend our main function in SparkExample.kt.

data class Person(val id: Long, val firstName: String, val lastName: String)

fun main(args: Array<String>) {
    val jdbc = Jdbc()
    createTablesAndData(jdbc)

    val gson = Gson()
    get("/", { _, _ -> "hello" })
    get("/person", { _, _ ->
        jdbc.queryForList("SELECT * FROM person", {
            Person(it.getLong("id"), it.getString("first_name"), it.getString("last_name"))
        })
    }, gson::toJson)
}

First we add a DTO for the person table that we can load the data into and then map to JSON. We use the queryForList method of our JDBC wrapper to load all people from the database and map the ResultSet (again, the it variable in the lambda is implicit by Kotlin for the parameter). Notice that we don’t need a return statement in the mapper lambda, the last line is automatically returned. Finally we tell Spark to use Gson as the transformer by referencing the toJson method of the instance.

Now when the application is started a curl http://localhost:4567/person should yield a JSON list of people.

Using Path Parameters to Load a Specific Person

Our next task is to load a specific person by id and search for people by last name. We will implement the endpoints GET /people/:id and GET /people?last_name=Schulze.

Let’s start at the bottom: We need JDBC wrapper code to load a single object and our methods need to accept parameters (e.g. the id or the last name).

class Jdbc {

    /**
     * Executes a query like an INSERT or UPDATE
     */
    fun execute(sql: String, vararg args: Any?) {
        callInternal(sql, {
            it.execute()
        }, *args)
    }   

    /**
     * Queries for a single object.
     * Throws an IllegalStateException if no row is found for the query.
     */
    fun <T> query(sql: String, mapper: (rs: ResultSet) -> T, vararg args: Any?): T {
        return callInternal(sql, {
            val rs = it.executeQuery()
            if (rs.next()) {
                mapper.invoke(rs)
            } else {
                throw IllegalStateException("No result found for query.")
            }
        }, *args)
    }

    /**
     * Queries the database for a list of objects.
     */
    fun <T> queryForList(sql: String, rowMapper: (rs: ResultSet) -> T, vararg args: Any?): List<T> {
        return callInternal(sql, {
            val result = ArrayList<T>()
            val rs = it.executeQuery()
            while (rs.next()) {
                result.add(rowMapper.invoke(rs))
            }
            result
        }, *args)
    }

    /**
     * Creates and cleans up a prepared statement with a user provided action on the statement.
     */
    private fun <T> callInternal(sql: String, action: (pstmt: PreparedStatement) -> T, vararg args: Any?): T {
        var conn: Connection? = null
        var pstmt: PreparedStatement? = null
        try {
            conn = dataSource.connection
            pstmt = conn.prepareStatement(sql)
            pstmt.setParameters(*args)
            return action.invoke(pstmt)
        } finally {
            conn?.close()
            pstmt?.close()
        }
    }
}

fun PreparedStatement.setParameters(vararg args: Any?) {
    for (i in args.indices) {
        val arg = args[i]
        val statementIndex = i + 1
        when(arg) {
            is String -> this.setString(statementIndex, arg)
            is Long -> this.setLong(statemendIndex, arg)
        }
    }
}

Let’s again look at some interesting lines.

  • Line 51: This is an extension method. Kotlin allows to add methods to existing objects. In this case it is not really necessary since we only use it in one place (line 42) but it is a good feature to have in mind. Also notice in line 56 that Kotlin automatically casts the arg variable to String for us since it know at compile time (through the is String check) that this is possible.
  • Line 16 and 30 and 42: You might be wondering why we write \*args with the asterisk. This is to tell the Kotlin compiler that we want to „spread“ the varargs to varargs of the method we call. If we don’t do this Kotlin will pass an Array<Any?> to the first vararg parameter of callInternal. With the asterisk the array is correctly expanded.

Now let’s extend our existing GET /person route and add a new one for GET /person/:id.

fun main(args: Array<String>) {
    val jdbc = Jdbc()
    createTablesAndData(jdbc)

    val personMapper = { rs: ResultSet -> Person(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")) }

    val gson = Gson()
    get("/", { _, _ -> "hello" })
    get("/person", { req, _ ->
        if(req.queryParams("last_name") != null) {
            jdbc.queryForList("SELECT * FROM person WHERE last_name = ?", personMapper, req.queryParams("last_name"))
        } else {
            jdbc.queryForList("SELECT * FROM person", personMapper)
        }
    }, gson::toJson)
    get("/person/:id", { req, _ ->
        jdbc.query("SELECT * FROM person WHERE id = ?", personMapper, req.params("id").toLong())
    }, gson::toJson)
}
  • In line 5 I have extracted the lambda to map from a ResultSet to a Person so I can reuse it.
  • Lines 10ff: We now check if the query parameter last_name is present (i.e. the value from /person?last_name=Foo). If so we use an SQL query that filters by last name and also use our new vararg parameters to set the parameter. If not we use the old select. Note that we now have an actual name for the first parameter of the lambda since we need to access the request.
  • Lines 16ff: This route has a parameter (not query parameter), the id of the person to load. It can be accessed via req.parmaters("id"). We will not handle the IllegalStateException when the id is not in the database for now.

Adding Static Resources

Now let’s add some HTML and JS to display the people to a users. I will not go very deep into data binding with some fancy JS framework, that would extend the scope too much. This part is mostly about adding static resources.

We create a new file src/main/resources/static/index.html.

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>People</title>
</head>
<body>
<h1>People</h1>
<button type="button" onclick="loadPeople()">Load</button>
<a href="/index.js">/index.js</a>
</body>
</html>

And src/main/resources/static/index.js. I’m using ECMAScript 6 here.

function loadPeople() {
    $ajax.get('/person')
        .then(people => {
            document.getElementById('result').textContent = people;
        });
}

let $ajax = {
    get(url) {
        return this.call('GET', url);
    },

    call(method, url) {
        return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest();
            xhr.open(method, url);
            xhr.onload = () => {
                if (xhr.status < 400) {
                    resolve(xhr.responseText)
                } else {
                    reject(new Error(xhr.responseText))
                }
            };
            xhr.onerror = () => reject(new Error("Network error"));
            xhr.send();
        });
    }
};

Let’s see how we can serve these resources with Spark.

fun main(args: Array<String>) {
    val jdbc = Jdbc()
    createTablesAndData(jdbc)

    val personMapper = { rs: ResultSet -> Person(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name")) }

    staticFiles.location("/static")
    redirect.get("/", "/index.html")
    val gson = Gson()
    get("/person", { req, _ -> ... })
    /// other route
}

In line 7 we tell Spark where on the classpath our static resources are located. On line 8 we add a redirect so index.html gets displayed when the user visits our page without a path. These two static objects come from spark.Spark. The old GET / route that displayed hello has been removed.

Now when you start the application and open http://localhost:4567 in your browser you should see the HTML page and can load the people with a button click.

Bonus: Creating a New Person

The last chapter will be to create a form on index HTML to create a new person.

Let’s create a new route to react to a POST /person request.

post("/person", { req, _ ->
    val person = gson.fromJson(req.body(), Person::class.java)
    jdbc.execute("INSERT INTO person (first_name, last_name) VALUES (?, ?)", person.firstName, person.lastName)
    halt(201)
})

We use Gson to map the request body to our DTO and then perform the insert. With halt(201) we immediately send a HTTP 201 response back.

We add a form to index.html.

<form onsubmit="createPerson(event)">
    <label for="first_name">First name</label>
    <input id="first_name" type="text" name="first_name" required="required">
    <label for="last_name">Last name</label>
    <input id="last_name" type="text" name="last_name" required="required">
    <button type="submit">Submit</button>
</form>

And the submit handler function to index.js. We also add support for POST requests in our small AJAX library.

function createPerson(event) {
    event.preventDefault();
    event.stopPropagation();
    $ajax.post('/person', {firstName: event.target.first_name.value, lastName: event.target.last_name.value});
}

let $ajax = {
    post(url, data) {
        return this.call('POST', url, data)
    },

    call(method, url, data) {
        return new Promise((resolve, reject) => {
            // ...
            if(data) {
                xhr.send(JSON.stringify(data));
            } else {
                xhr.send()
            }
        });
    }
};

We added a data parameter to be able to send a body with a request.

And that’s it! Now you can also create new people with your shiny Spark app.

If you want to deploy it, just use gradle build and copy the JAR file to a server. In a later blog post I will look into options to run our Spark app as a service with Linux.

Conclusion

Writing a REST service with Spark and Kotlin is very fast and concise. When database access is needed and is kept small no additional framework is needed. The code of this article fits into a 2.9MB JAR and starts in roughly 1 second!

Stay in the Loop

If you would like to receive an email every now and then with new articles, just sign up below. We will never spam you!