Web Services

Overview

Up to this point, we’ve assumed that we’re designing a standalone application i.e. for a single computer. Historically, many software applications were designed to be standalone, and much of the software that we use is still standalone.

However, it can be useful to sometimes split processing across multiple systems. Here’s a partial list of the reasons why you might want to do this:

  • Resource sharing. We often need to share resources across users. For example, storing our customer data in a shared databases that everyone in the company can access.
  • Reliability. We might want to increase the reliability of our software by redunant copies running on different systems. This allows for fault tolerance - failover in case the first system fails. This is common with important resources like a web server.
  • Performance. It can be more cost effective to have one highly capable machine running the bulk of our processing, while cheaper/smaller systems can be used to access that shared machine. It can also be cheaper to spread computation across multiple systems, where tasks can be run in parallel. Distributed architectured provide flexibility to align the processing capabilities with the task to be performed.
  • Scalability. Finally, if designed correctly, distributing our work across multiple systems can allow us to grow our system to meet high demand. Amazon for example, needs to ensure that their systems remain responsive, even in times of heavy load (e.g. holiday season).
  • Openness. There is more flexibility, since we can mix systems from different vendors.

Earlier, we discussed a distributed application as a set of components, spread across more than one machine, and communicating with one another over a network. Each component has some capabilities that it provides to the other components, and many of them coordinate work to accomplish a specific task.

We have already described a number of different distributed architectures. These enforce the idea that distributed systems can take many different forms, each with it’s own advantages and disadvantages. When we’re considering building a service, we’re really focusing on a particular kind of distributed system, where our application is leveraging remote resources.

Service Architectures

Client-Server

A client-server architecture is a model where one centralized server provides capabilities to multiple client machines.

  • Client − The process that issues a request to the second process i.e. the server. From the point of view of the user, this is the application that they interact with.
  • Server − The process that receives the request, carries it out, and sends a reply to the client.

Client–server model - Wikipedia Client–server model - Wikipedia

In this architecture, the application is modelled as a set of services that are provided by servers and a set of clients that use these services. The servers need not know about clients, but the clients must know the identity of servers.

Advantages

  • Separation of responsibilities such as user interface presentation and business logic processing (client).
  • Reusability of server components and potential for concurrency (single server).
  • It also makes effective use of resources when a large number of clients are accessing a high-performance server.

Disadvantages

  • Limited server availability and reliability.
  • Limited testability and scalability.
  • Fat clients with presentation and business logic together.
  • Limited ability to scale the server (need more processing, “buy a bigger server”).

Multi-tier architecture

A multi-tier architecture (also known as 2-tier, 3-tier or layered) is an architecture that separates an application into separate tiers or areas of concerns1. Often the user interface, business logic and data layers end up split apart.

The most common form is this architecture is 3-tier architecture, where each tier is a separate module, deployed on a separate computer. Tiers typically communicate over a network connection.

  1. The top tier is the Presentation layer, which handles the UI logic. This is typically hosted on a client machine (i.e. where the user accesses it).
  2. The middle layer is the Application or Logic tier, which handles “business logic”; the rules and state management of the application. This can often include logic for coordinating requests from a client across multiple services as well.
  3. The bottom layer is the Data tier, which handles access, storage and management of the underlying data. This is often a database, or a wrapper around a database (or some other form of storage).

Three-tier architecture - Wikipedia Three-tier architecture - Wikipedia

This is an extremely common architecture for Enterprise applications. It also aligns with the way that websites are traditionally served (Presentation tier is the browser, Logic tier is the web server and underlying code, and the Data tier is a database).

In some cases, the Logic and Presentation tiers are combined, for a 2-tier architecture consisting of just Presentation and Data tiers.

Advantages

  • Enhances the reusability and scalability − as demands increase, extra servers can be added.
  • Provides maintainability and flexibility.

Disadvantages

  • Greater complexity, more difficult to deploy and test across tiers.
  • More emphasis on server reliability and availability.

Service Oriented Architecture

A service-oriented architecture (SOA) is an architectural style that supports service orientation. A service is a discrete unit of functionality that can be accessed remotely and acted upon and updated independently, such as retrieving a credit card statement online.

In other words, services exist independently of clients, and provide services to any client that requires it. Services are loosely coupled to one another, and should act independently.

Principles of SOA, governing how services should be designed:

  • Service contract: there should be an agreed upon interface for accessing a service.
  • Longevity: services should be designed to be long-lived (long-running).
  • Autonomy: services should work independently of one another.
  • Service composibility: services can be used to compose other services.
  • Stateless: services should not track state, but either return a resulting value or throw an exception if necessary.

Because they are independent entities, we need a supporting infrastructure around services, and applications that are designed to leverage that infrastructure. This includes a repository, where an application can search for services that can meet its needs at runtime.

SOA Elements SOA Elements

Advantages

  • A client or any service can access other services regardless of their platform, technology, vendors, or language implementations.
  • Each service component is independent from other services due to the stateless service feature.
  • The implementation of a service will not affect the application of the service as long as the exposed interface is not changed.
  • Enhances the scalability and provides standard connection between systems.

Disadvantages

  • Even more complexity in setting up a system, since we’re now distributing across multiple tiers.
  • Registry and other supporting infrastructure can be complex to setup and maintain.
  • Difficulty debugging, profiling and so on.

Microservices

A microservices architecture arranges an application as a collection of loosely coupled services, using fine-grained services and a lightweight protocol. Some of the defining characteristics of microservices:

  • Services are organized around business capabilities i.e. they provide specialized, domain-specific services to applications (or other services).
  • Service are not tied to any one programming language, platform or set of technologies.
  • Services are small, decentralized, and independently deployable.

A microservice based architecture is really a subtype of SOA, which an emphasis on smaller, domain-specific components with very narrow functions.

Microservices Microservices

Advantages

  • Easier to design, build and deploy small targeted services.
  • Redundancy - you can always “spin up” a replacement service if something fails.
  • Performance - you can always “scale out” by firing up redundant services to share the workload, as required.

Disadvantages

  • Extremely difficult to test and debug.
  • Practically requires supporting services, like a registry for processes to locate service endpoints.

Web Servers

As originally designed, a web server and web browser are a great example of a client-server architecture.

A web server is service running on a server, listening for requests at a particular port over a network, and serving web documents (HTML, JSON, XML, images). The payload that is delivered to a web browser is the content, which the browser interprets and displays. When the user interacts with a web page, the web browser reacts by making requests for additional information to the web server.

Over time, both browser and web server have become more sophisticated, allowing servers to host additional content, run additional programs as needed, and work as part of a larger ecosystem that can distribute client requests across other systems.

Web technologies are interesting to us because they can be the basis for a robust service request mechanism. We’ll explore that in this section.

HTTP Protocol

The Hypertext Transfer Protocol (HTTP) is an application layer protocol that supports serving documents, and processing links to related documents, from a remote service.

HTTP functions as a request–response protocol:

  • A web browser is a typical client, which the user is accessing. A web server would be a typical server.
  • The user requests content through the browser, which results in an HTTP request message being sent to the server.
  • The server, which provides resources such as HTML files and other content or performs other functions on behalf of the client, returns a response message to the client. The response contains completion status information about the request and may also contain requested content in its message body.

HTTP Request/Response - ntu.edu HTTP Request/Response - ntu.edu

Request Methods

HTTP defines methods to indicate the desired action to be performed on the identified resource.

What this resource represents, whether pre-existing data or data that is generated dynamically, depends on the implementation of the server. Often, the resource corresponds to a file or the output of an executable residing on the server. Method names are case sensitive.

  • GET: The GET method requests that the target resource transfers a representation of its state. GET requests should only retrieve data and should have no other effect.

  • HEAD: The HEAD method requests that the target resource transfers a representation of its state, like for a GET request, but without the representation data enclosed in the response body. Uses include looking whether a page is available through the status code, and quickly finding out the size of a file (Content-Length).

  • POST: The POST method requests that the target resource processes the representation enclosed in the request according to the semantics of the target resource. For example, it is used for posting a message to an Internet forum, or completing an online shopping transaction.

  • PUT: The PUT method requests that the target resource creates or updates its state with the state defined by the representation enclosed in the request. A distinction to POST is that the client specifies the target location on the server.

  • DELETE: The DELETE method requests that the target resource deletes its state.

Spring Boot

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.

Setup

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

Info

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.

Info

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 H2 database with backing table

Info

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

Making 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.

Packaging

Unlike applications, which are hosted by users on their own systems, services are typically deployed on servers. These can be physical systems, VMs or containers running in the cloud, or any combination of these targets.

Web services, the type that we’ve been considering, need to be deployed to a web server. When we were building projects with Spring Boot, it quietly launched a web server in the background to support us testing our application. To deploy in a production environment though, we would need to install our application in an environment where a web server is already installed.

From Spring, we can produce one of two packages:

  • WAR file: a Web Application Archive (WAR) file is a standard deployment package for services that run on a web server.
  • JAR file: produce a standalone JAR file and deploy it (see instructions above).

We’ll focus on the most common situation: packaging for distribution to a standalone web server, or cloud platform. This isn’t something that we will be doing in this course, but it’s helpful to know that there is an easy road to deploy your application in a production environment.

JAR Archives

The sections below are taken in part from the official Spring deployment documentation.

Spring Boot’s executable jars are ready-made for most popular cloud PaaS (Platform-as-a-Service) providers. These providers tend to require that you “bring your own container”. They manage application processes (not Java applications specifically), so they need an intermediary layer that adapts your application to the cloud’s notion of a running process.

Amazon Web Services (AWS)

Amazon Web Services offers multiple ways to install Spring Boot-based applications, either as traditional web applications (war) or as executable jar files with an embedded web server. The options include:

  • AWS Elastic Beanstalk
  • AWS Code Deploy
  • AWS OPS Works
  • AWS Cloud Formation
  • AWS Container Registry

As an example, AWS Elastic Beanstalk allows deploying a WAR file directly to a Tomcat Platform (a well-known web/application server), or the “Java SE Platform”. For web services, we would follow their documentation for configuring the cloud platform, and then upload the WAR file that we produce in IntelliJ.

Google Cloud

Google Cloud has several options that can be used to launch Spring Boot applications. The easiest to get started with is probably App Engine, but you could also find ways to run Spring Boot in a container with Container Engine or on a virtual machine with Compute Engine.

To run in App Engine, you can create a project in the UI first, which sets up a unique identifier for you and also sets up HTTP routes. Add a Java app to the project and leave it empty and then use the Google Cloud SDK to push your Spring Boot app into that slot from the command line or CI build.

App Engine Standard requires you to use WAR packaging.

Standalone Service

Spring also supports running your service as a standalone application. This is very similar to what we did above, when we generated scripts and a JAR file to launch our application. For details, refer to the Spring documentation.


  1. The terms “tier” and “layer” are sometimes used interchangeably, but they do not mean the same thing. Layer is a logical structuring mechanism, while a tier represents physical structuring. These do not necessarily need to align perfectly, as a logical layer (for example) may be distributed across physical systems, especially at large-scale. ↩︎