CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Spring Framework

Spring

Spring is a popular Java framework. It’s opinionated, in that it provides a strict framework for a web service. You are expected to add classes, and customize behaviour as needed, but you are restricted to the overarching structure that is provided. This is a reasonable tradeoff. In return for giving up some flexibility, you get Dependency Injection and other advanced features, which make testing (along other things) much easier to achieve.

This power comes at a price: complexity. Spring has a large number of configuration files that allow you to tweak and customize the framework, but the options can be overwhelming. To help developers, Pivotal created Spring Boot, which is a program that creates a starting configuration for you - even going so far as to include a web server and other required libraries to get you up-and-running quickly.

We’ll walk through setting up a simple Spring Boot application. The steps are typically:

  • Use Spring Boot to create a working project.
  • Write controller, model, and other required classes.
  • Write and run tests.

Spring Boot

It is highly recommended that you use Spring Boot to create your starting project. You can run it one of two ways:

  • Visit start.spring.io and use the web form to set the parameters for your project. Generate a project and download the project files.
  • Use the Spring project wizard in IntelliJ. Set the parameters for your project (Kotlin, Gradle) and follow instructions to generate an IntelliJ IDEA project.

Creating a Spring project in IntelliJ IDEA

Regardless of which you choose, you will be asked for dependencies. You will probably want to include at least Spring Web and Spring Data JPA and possibly others:

  • Spring Web: this will embed a web server in your project so that you can easily test it.
  • Spring Data JPA: JPA stands for Java Persistance API. JPA is an object-persistence layer that allows you to map classes directly to database tables and avoid writing SQL for simple requests.
  • JDBC API: this will allow you to use JDBC to access databases - helpful if your service needs to persist data.
  • H2 Database: an embedded database for testing - you can also swap out for a different database if desired.

Your starting project should look something like this:

Starting Spring project

Notice that the class is annotated with @SpringBootApplication. This tells the framework to treat this as the top-level application class. Spring uses extensive annotations like this to flag methods and properties for the framework.

The main() method calls the framework’s runApplication method to launch.

Your Spring project comes with an embedded web server, which runs on port 8080. You can test this by running the web server (click on the Play button beside the Main method), and then open the URL in a web browser: http://localhost:8080

The Spring Web starter code always defaults to https://localhost:8080 to serve your web service. Use this URL for testing.

Unfortunately, this won’t return anything useful yet. Although the web service is running, we need to write code to handle the requests! That’s the job of the controller.

We’ll add a controller class to save messages (just a couple of strings), or retrieve a list of messages that were previously posted.

Writing a Controller Class

The controller class is responsible for handling requests from a client process. Our web service uses HTTP, so requests will be the request methods that we discussed earlier: GET to retrieve a list of messages, and POST to store a new message.

Here’s an example of a controller class, configured to handle Post and Get requests.

@RestController
@RequestMapping("/messages")
class MessageResource(val service: MessageService) {
    @GetMapping
    fun index(): List<Message> = service.findMessages()

    @PostMapping
    fun post(@RequestBody message: Message) {
        service.post(message)
    }
}

@RestController flags this class as our main controller class, which will be responsible for handling the HTTP requests. Within this class, we need to write code to handle the endpoints and requests for our application. The main endpoint will be /messages, since we set that mapping on the controller directly.

@GetMapping and @PostMapping indicate the methods that will handle GET and POST requests respectively. Our methods work with MessageService and Message classes, which we will need to define.

This means that our endpoint will be https://localhost:8080/messages (the default address and port for Spring, plus the endpoint that we defined). Our controller will handle GET and POST requests to that endpoint.

The MessageResource() class declaration is an example of dependency injection: instead of instantiating the MessageService inside of the class, we pass it in as a parameter. MessageService is flagged as a class that the framework can manage directly.

Dependency injection makes testing earlier, since you don’t have unmanaged objects being allocated inside of a class. In this case, we can mock the MessageService during testing to isolate our MessageResource tests.

We can identify the following annotations:

Annotation Use
@RestController Indicates a controller class that should process requests
@GetMapping A function that will be called when a GET request is received.
@PostMapping A function that will be called when a POST request is received.

Using this code:

  • A client sending a GET request will be returned a list of all messages in JSON format.

  • A client sending a POST request, with well-formed data, will create a new message.

We could add in other mappings (e.g. PUT, DELETE) if required. All of these mappings would be handled in the Controller class.

We can finish our first pass at this service by adding a Message class and a MessageService class to store the data from our requests. The full service is listed below (also in the public repo: /service/spring-server-basic)

package demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class DemoApplication

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

@RestController
class MessageResource(val service: MessageService) {
    @GetMapping
    fun index(): List<Message> = service.findMessages()

    @PostMapping
    fun post(@RequestBody message: Message) {
        service.post(message)
    }

}

data class Message(val id: String, val text: String)

@Service
class MessageService {
    var messages: MutableList<Message> = mutableListOf()

    fun findMessages() = messages
    fun post(message: Message) {
        messages.add(message)
    }
}

Using JPA for Object Mapping

So far, we’re receiving and storing JSON objects as Messages. However, we’re only saving them in a list, which will be lost when we halt our service. What if we want to persist the data into a database? How do we convert our Message objects to a format that we can write out?

Spring Data JPA is a library that focuses on using JPA to store data in a relataional database. It greatly simplifies setting up a repository: we just setup the interface that we want, and it automatically creates the implementation code for us!

To start, add the JAP dependencies to your build.gradle file. Make sure to add these to the existing sections - don’t delete anything! Once added, click on the Sync icon

plugins {
	kotlin("plugin.jpa") version "1.6.10"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}

Next we’ll add a repository interface that tells the service what operations we want to perform against our Message class. The modified code looks like this:

@Service
class MessageService(val db: MessageRepository) {

    fun findMessages(): List<Message> = db.findMessages()

    fun post(message: Message){
        db.save(message)
    }
}

interface MessageRepository : CrudRepository<Message, String>{

    @Query("select * from messages")
    fun findMessages(): List<Message>
}

@Table("MESSAGES")
data class Message(@Id val id: String?, val text: String)

By inheriting from CrudRepository, the MessageRepositorygains methods for save, findByIndex and other common operations.

The findMessages method is undefined, so we use some annotations to map it to a query against a specific database table.

  • @Query indicates a database query that should be run, and the results mapped to the output of this function.
  • @Table describes a data class that corresponds to an underlying database table. This table will be created for us, using the parameters from the Message class as column names.

By default, our Spring framework projects support H2, and in-memory database. The database file is stored in ./data in our project folder. You can browse it in IntelliJ IDEA and see the underlying table that is created.

H2 database with backing table

Final working code can be downloaded from the public repository: https://git.uwaterloo.ca/j2avery/cs346-public/-/tree/master/service/spring-server-jpa

Generating Requests

We can do a lot with just POST and GET operations, since we also have the ability to pass parameters in our requests. If your server is running in IntelliJ IDEA, you can create requests for testing directly in the IDE:

  1. Click the drop-down menu beside the GET or POST mapping in your Controller code. You should see an option to

Generate requests for testing

  1. This should bring up a script where you can enter requests that you wish to run. Enter as many as you wish: to run them, click on the Run arrow beside the request. Pay careful attention to the format, and notice the content type is included in the request:

Test using HTTP requests in IntelliJ IDEA

You can run simple tests directly from a browser, but IntelliJ IDEA for testing provides additional support like code completion and syntax highlighting for structuring your requests. This doesn’t replace proper automated tests, but it’s certainly helpful when configuring and debugging your services.

Client Requests

The big question, of course, is how do we make requests to this service from a client? How do we actually use it?

Kotlin (and Java) includes libraries that allow you to structure and execute requests from within your application.

This example opens a connection, and prints the results of a simple GET request:

URL("https://google.com").readText()

To use our service, we may want to set a few more parameters on our request. The HttpRequest class uses a builder to let us supply as many optional parameters as we need when building the request.

Here’s a method that fetches data from our server example above:

fun get(): String {
    val client = HttpClient.newBuilder().build()
    val request = HttpRequest.newBuilder()
        .uri(URI.create("http://127.0.0.1:8080"))
        .GET()
        .build()

    val response = client.send(request, HttpResponse.BodyHandlers.ofString())
    return response.body()
}

We often need to package data into our requests. Here is an example of a POST request sending JSON data to our service, and returning the response:

fun post(message: Message): String {
    val string = Json.encodeToString(message)

    val client = HttpClient.newBuilder().build();
    val request = HttpRequest.newBuilder()
        .uri(URI.create("http://127.0.0.1:8080"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(string))
        .build()

    val response = client.send(request, HttpResponse.BodyHandlers.ofString());
    return response.body()
}

GET and POST represent the most common requests. You can similarly support other request types that have been discussed.

See the public repo for these samples, under /service/sprint-client and /service/spring-server.