Build a Spring Boot Application on Pivotal Web Services and scale it with Memcache

In this tutorial we’ll learn how to create a simple Spring Boot 2 application (based on the Spring Framework 5), deploy it to Pivotal Web Services, then add Memcache to alleviate a performance bottleneck.

Memcache is a technology that improves the performance and scalability of web apps and mobile app backends. You should consider using Memcache when your pages are loading too slowly or your app is having scalability issues. Even for small sites, Memcache can make page loads snappy and help future-proof your app.

Prerequisites

Before you complete the steps in this guide, make sure you have all of the following:

Deploying a Spring Boot application to Pivotal Web Services

To easily create a Spring Boot application we recommend you to install the Spring Boot CLI. If you don’t want to install the CLI you can also configure and download a Spring Boot skeleton from the Spring Initializer.

There is a ruby on rails application called spring as well. In case you have it installed or in case you have the Ruby version manager rbenv installed which shims this binary, create an alias for the spring cli (e.g., alias springboot='/opt/spring-boot-cli/bin/spring').

With the CLI you can easily create a skeleton:

$ spring init --d=web,data-jpa,thymeleaf -g com.memcachier -a tutorial -n TaskList memcachier_tutorial
$ cd memcachier_tutorial

The created skeleton is a web app with database support (data-jpa) and using the thymeleaf template language. Spring Boot also supports other template languages such as JSP, groovy, freemaker, and mustache.

Create a Pivotal Web Services app

Turning this Spring Boot skeleton into a PWS app is easily done with 2 simple steps:

  1. In order to let PWS know how to start up your app, you need to add a manifest.yml file that contains:

    ---
    applications:
      - name: memcachier-tutorial
        buildpack: java_buildpack
        path: target/tutorial-0.0.1-SNAPSHOT.jar
        random-route: true
  2. Build and push the application:

    $ mvn package -Dmaven.test.skip=true
    $ cf push

    This will create an app called memcachier-tutorial. Note that the app will currently fail to start. The reason is that a Spring Boot app initialized with database support (data-jpa) will not start without a valid database configuration.

    Note: To push your application you must be logged in. You can log into PWS with cf login -a api.run.pivotal.io -u <USERNAME>.

Add task list functionality

Let’s add a task list to the app that enables users to view, add, and delete tasks. To accomplish this, we need to:

  1. Set up the database
  2. Create a Task entity and a table to store them
  3. Create the view and controller logic

Set up a PostgreSQL database

Before we can configure a database in Spring Boot, we need to create the database. On PWS, you can add a free database from ElephantSql to your app like so:

$ cf create-service elephantsql turtle mc-tutorial-psql
$ cf bind-service memcachier-tutorial mc-tutorial-psql

This creates a PostgreSQL database and binds it to the memcachier-tutorial app. This also adds an elephantsql entry in the VCAP_SERVICES environment variable that contains the URL to connect to the database.

For the database to work out of the box Spring Boot requires the variable SPRING_DATASOURCE_URL to be set. This variable contains the same URL as the one set in VCAP_SERVICES except that it starts with jdbc:postgresql instead of postgres. PWS will however automatically configure the database service connection in Spring so you don’t have to worry about it.

To use this database we need to install a few packages. Add the following dependencies in pom.xml:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
</dependency>
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.3.0</version>
</dependency>
<dependency>
  <groupId>org.liquibase</groupId>
  <artifactId>liquibase-core</artifactId>
  <version>3.6.1</version>
</dependency>
  • The first dependency simply is the PostgreSQL driver.
  • The second dependency just adds the JAXB APIs, as they are no longer available out of the box for newer Java SE versions. For more information see this Stack Overflow thread.
  • The third dependency allows you to create and run liquibase database migrations.

Now we can configure the database in src/main/resources/application.properties:

spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.maxActive=10
spring.datasource.maxIdle=5
spring.datasource.minIdle=2
spring.datasource.initialSize=5
spring.datasource.removeAbandoned=true

# Supress exception regarding missing PostgreSQL CLOB feature at Spring startup.
# See http://vkuzel.blogspot.ch/2016/03/spring-boot-jpa-hibernate-atomikos.html
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect

Your PostgreSQL database is now ready to be used.

Create the Task entity and database table

In order to create and store tasks we need to create three things: a Task entity, a repository to teach Spring Boot how to store and retrieve tasks, and a migration that creates the actual table in the database.

  1. Add the Task entity to src/main/java/com/memcachier/tutorial/Task.java:

    package com.memcachier.tutorial;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    import org.hibernate.validator.constraints.NotEmpty;
    
    @Entity
    public class Task {
    
      @Id
      @GeneratedValue(strategy=GenerationType.IDENTITY)
      private Long id;
      @NotEmpty
      private String name;
    
      protected Task() {}
    
      public Task(String name) {
        this.name = name;
      }
    
      public Long getId() {
        return this.id;
      }
    
      public String getName() {
        return this.name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      @Override
      public String toString() {
        return String.format("Task[id=%d, name='%s']", this.id, this.name);
      }
    
    }
  2. Create a repository in src/main/java/com/memcachier/tutorial/TaskRepository.java:

    package com.memcachier.tutorial;
    
    import java.util.List;
    import org.springframework.data.repository.CrudRepository;
    
    public interface TaskRepository extends CrudRepository<Task, Long> {}

    If you need more than basic CRUD functions to access your data you can also extend a PagingAndSortingRepository or a JpaRepository instead. See this StackOverflow thread for more information.

  3. Create a liquibase migration in src/main/resources/db/changelog/db.changelog-master.yaml:

    databaseChangeLog:
      - changeSet:
          id: 1
          author: memcachier
          changes:
            - createTable:
                tableName: task
                columns:
                  - column:
                      name: id
                      type: int
                      autoIncrement: true
                      constraints:
                        primaryKey: true
                        nullable: false
                  - column:
                      name: name
                      type: varchar(255)
                      constraints:
                        nullable: false

    Note, you will need to create the db and changelog folders. The migration will run automatically when the application starts.

Create the task list application

The actual application consists of a view that is displayed in the frontend and a controller that implements the functionality in the backend.

  • Create a controller in src/main/java/com/memcachier/tutorial/TaskController.java:

    package com.memcachier.tutorial;
    
    import javax.validation.Valid;
    import java.lang.Iterable;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.ModelMap;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @Controller
    @RequestMapping("/")
    public class TaskController {
    
      private TaskRepository taskRepo;
    
      @Autowired
      public TaskController(TaskRepository repo) {
        this.taskRepo = repo;
      }
    
      @RequestMapping(method = RequestMethod.GET)
      public String showAllTasks(ModelMap model) {
        Iterable<Task> tasks = this.taskRepo.findAll();
        model.addAttribute("tasks", tasks);
        model.addAttribute("newTask", new Task());
        return "task";
      }
    
      @RequestMapping(method = RequestMethod.POST)
      public String newTask(ModelMap model,
                            @ModelAttribute("newTask") @Valid Task task,
                            BindingResult result) {
        if (!result.hasErrors()) {
          this.taskRepo.save(task);
        }
        return showAllTasks(model);
      }
    
      @RequestMapping(method = RequestMethod.DELETE)
      public String deleteTask(ModelMap model, @RequestParam("taskId") Long id) {
        this.taskRepo.deleteById(id);
        return showAllTasks(model);
      }
    }

    This controller contains all functionality to GET all tasks and render the task view, to POST a new task that will then be saved to the database, and to DELETE existing tasks.

  • Create a view in src/main/resources/templates/task.html:

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
      <head>
        <title>MemCachier Spring Boot Tutorial</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    
        <!-- Fonts -->
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css"
              rel='stylesheet' type='text/css' />
    
        <!-- Bootstrap CSS -->
        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
              rel="stylesheet" />
      </head>
    
      <body>
        <div class="container">
    
          <!-- New Task Card -->
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">New Task</h5>
    
              <form th:object="${newTask}" method="POST">
                <div class="form-group">
                  <input type="text" class="form-control"
                         placeholder="Task Name" th:field="*{name}" />
                </div>
                <button type="submit" class="btn btn-default">
                  <i class="fa fa-plus"></i> Add Task
                </button>
              </form>
            </div>
          </div>
    
          <!-- Current Tasks -->
          <div th:if="${not #lists.isEmpty(tasks)}">
            <div class="card">
              <div class="card-body">
                <h5 class="card-title">Current Tasks</h5>
    
                <table class="table table-striped">
                  <tr th:each="task : ${tasks}">
                    <!-- Task Name -->
                    <td th:text="${task.name}" class="table-text"></td>
                    <!-- Delete Button -->
                    <td>
                      <form th:object="${deleteTask}" th:method="DELETE">
                        <input type="hidden" name="taskId" th:value="${task.id}">
                        <button type="submit" class="btn btn-danger">
                          <i class="fa fa-trash"></i> Delete
                        </button>
                      </form>
                    </td>
                  </tr>
                </table>
              </div>
            </div>
          </div>
        </div>
    
        <!-- Bootstrap related JavaScript -->
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
      </body>
    </html>

    The view basically consists of two cards, one that contains a form to create new tasks and another containing a table with the existing tasks and a delete button to remove the corresponding task.

Let us see what we have done so far and deploy the task list to PWS:

$ mvn package -Dmaven.test.skip=true
$ cf push

Check out the route (URL) to access the app via cf app memcachier-tutorial. It should be something like memcachier-tutorial-<RANDOM NAME>.cfapps.io. Open the URL in your browser and test the application by adding a few tasks. We now have a functioning task list running on PWS. With this complete, we can learn how to improve its performance with Memcache.

Add caching to Spring Boot

Memcache is an in-memory, distributed cache. Its primary API consists of two operations: SET(key, value) and GET(key). Memcache is like a hashmap (or dictionary) that is spread across multiple servers, where operations are still performed in constant time.

The most common use for Memcache is to cache expensive database queries and HTML renders so that these expensive operations don’t need to happen over and over again.

Set up Memcache

To use Memcache in Spring Boot, you first need to provision an actual Memcache cache. You can easily get one for free with the MemCachier service:

$ cf create-service memcachier dev mc-tutorial-memcached
$ cf bind-service memcachier-tutorial mc-tutorial-memcached

Then we need to configure the appropriate dependencies. We will use simple-spring-memcached with XMemcached to use Memcache within Spring Boot. You can use also simple-spring-memcached with SpyMemcached. If you wish to do so, please refer to the MemCachier documentation.

To use simple-spring-memcached add the following to your pom.xml:

<dependency>
  <groupId>com.google.code.simple-spring-memcached</groupId>
  <artifactId>xmemcached-provider</artifactId>
  <version>4.0.0</version>
</dependency>
<!-- Force XMemcached to version 2.4.3 (simple-spring-memcached uses 2.4.0) -->
<dependency>
  <groupId>com.googlecode.xmemcached</groupId>
  <artifactId>xmemcached</artifactId>
  <version>2.4.3</version>
</dependency>

<!-- JSON parsing to get MemCachier credentials -->
<dependency>
  <groupId>org.json</groupId>
  <artifactId>json</artifactId>
  <version>20180130</version>
</dependency>

Now we can configure Memcache for Spring in src/main/java/com/memcachier/tutorial/MemCachierConfig.java:

package com.memcachier.tutorial;

import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.json.*;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.google.code.ssm.CacheFactory;
import com.google.code.ssm.config.AbstractSSMConfiguration;
import com.google.code.ssm.config.DefaultAddressProvider;
import com.google.code.ssm.providers.xmemcached.XMemcachedConfiguration;
import com.google.code.ssm.providers.xmemcached.MemcacheClientFactoryImpl;
import net.rubyeye.xmemcached.auth.AuthInfo;
import net.rubyeye.xmemcached.utils.AddrUtil;

@Configuration
public class MemCachierConfig extends AbstractSSMConfiguration {

  @Bean
  @Override
  public CacheFactory defaultMemcachedClient() {
    String vcap_services = System.getenv("VCAP_SERVICES");
    String memcachier_servers = "localhost:11211";
    String memcachier_username = "";
    String memcachier_password = "";

    if (vcap_services != null && vcap_services.length() > 0) {
        // parsing MemCachier credentials
        JSONObject root = new JSONObject(vcap_services);
        JSONArray mc = root.getJSONArray("memcachier");
        JSONObject credentials = mc.getJSONObject(0).getJSONObject("credentials");
        memcachier_servers = credentials.getString("servers");
        memcachier_username = credentials.getString("username");
        memcachier_password = credentials.getString("password");
    }

    String serverString = memcachier_servers.replace(",", " ");
    List<InetSocketAddress> servers = AddrUtil.getAddresses(serverString);
    AuthInfo authInfo = AuthInfo.plain(memcachier_username,
                                       memcachier_password);
    Map<InetSocketAddress, AuthInfo> authInfoMap =
      new HashMap<InetSocketAddress, AuthInfo>();
    for(InetSocketAddress server : servers) {
      authInfoMap.put(server, authInfo);
    }

    final XMemcachedConfiguration conf = new XMemcachedConfiguration();
    conf.setUseBinaryProtocol(true);
    conf.setAuthInfoMap(authInfoMap);

    final CacheFactory cf = new CacheFactory();
    cf.setCacheClientFactory(new MemcacheClientFactoryImpl());
    cf.setAddressProvider(new DefaultAddressProvider(serverString));
    cf.setConfiguration(conf);
    return cf;
  }
}

This configures simple-spring-memcached which allows you to use its caching annotations. Spring also provides built in caching annotations that can be enabled via simple-spring-memcached. However, in this tutorial we will use the annotations provided simple-spring-memcached because they are generally more flexible and better suited for a Memcached backed cache. Nevertheless, this tutorial would work just as well with Spring’s annotations. If you prefer to use Spring’s built in caching annotations, please refer to the MemCachier documentation.

Cache expensive database queries

Memcache is often used to cache expensive database queries. In this simple example we do not have any expensive queries but for the sake of learning, let’s assume that getting all tasks from the database is an expensive operation.

To cache the Task queries we will extend the TaskRepository with methods that implement caching. Extending a repository in Spring Boot involves three steps:

  1. Build an interface with the methods that should be added to the TaskRepository in src/main/java/com/memcachier/tutorial/CachedTaskRepository.java:

    package com.memcachier.tutorial;
    
    import java.lang.Iterable;
    
    public interface CachedTaskRepository {
    
      public Iterable<Task> findAllCached();
    
    }
  2. Create a an implementation for this interface in src/main/java/com/memcachier/tutorial/TaskRepositoryImpl.java:

    package com.memcachier.tutorial;
    
    import java.lang.Iterable;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import com.google.code.ssm.api.ReadThroughAssignCache;
    
    public class TaskRepositoryImpl implements CachedTaskRepository {
    
      @Autowired
      TaskRepository taskRepository;
    
      @ReadThroughAssignCache(namespace="Taskrepo", assignedKey="all")
      public Iterable<Task> findAllCached() {
        return this.taskRepository.findAll();
      }
    }

    The filename of the implementation must follow the naming convention <REPOSITORY-NAME>Impl.java.

    You can access the rest of the CRUD interface of the TaskRepository by just adding an @Autowired reference to it.

    The caching occurs here via the @ReadThroughAssignCache annotation. All @ReadThrough*Cache annotations do the following:

    • Check if value is in cache and if true return said value.
    • If not in cache, execute function, return its value and store said value in the cache.

    The Assign version of this annotation will use an assigned key that is declared in the annotation. For more information about the annotations, refer to the Simple Spring Memcached documentation.

  3. Make sure this implementation is integrated into the TaskRepository. This is simply done by making the TaskRepository interface also extend the CachedTaskRepository interface:

    // ...
    public interface TaskRepository extends CrudRepository<Task, Long>, CachedTaskRepository {}

A note on caching annotations: Spring uses proxies to handle caching annotations. For this reason you cannot create a private method inside your controller, add a caching annotation and expect the method to be cached. In simple terms, the cached method must be part of a component that is accessed via it’s interface. For more information see this Stackoverflow thread and the therein mentioned references.

Now we have the methods to cache all tasks but in order for them to work the Task data type in src/main/java/com/memcachier/tutorial/Task.java needs to be serializable:

import java.io.Serializable;
// ...

public class Task implements Serializable {
  // ...
}

Finally, we can now get the cached tasks in the controller in src/main/java/com/memcachier/tutorial/TaskController.java:

// ...
public String showAllTasks(ModelMap model) {
  Iterable<Task> tasks = this.taskRepo.findAllCached();
  // ...
}
// ...

Let us deploy and test this new functionality:

$ mvn package -Dmaven.test.skip=true
$ cf push

To see what is going on in your cache, open the MemCachier dashboard. You can find the dashboard URL via cf service mc-tutorial-memcached.

The first time you loaded your task list you should have gotten an increase for the get misses and the set commands. Every subsequent reload of the task list should increase the get hits (refresh the stats in the dashboard).

Our cache is working but there is still a mayor problem. Add a new task and see what happens. No new task appears on our current tasks list. The new task was created in our database but our app is serving the stale task list from the cache.

Clear stale data

As important as caching data, is to invalidate it when it becomes stale. In our example the cached task list becomes stale whenever a new task is added or an existing task is removed. We need to make sure our cache is invalidated whenever one of these two actions are performed.

We can add wrappers to the save and delete methods in the TaskRepository that clear the cache with the following two steps:

  1. Declare the wrapper methods in the CachedTaskRepository interface in src/main/java/com/memcachier/tutorial/CachedTaskRepository.java:

    // ...
    public interface CachedTaskRepository {
      public Iterable<Task> findAllCached();
      public Task saveAndClearCache(Task t);
      public void deleteByIdAndClearCache(Long id);
    }
  2. Implement the wrapper methods in src/main/java/com/memcachier/tutorial/TaskRepositoryImpl.java:

    // ...
    import com.google.code.ssm.api.InvalidateAssignCache;
    
    public class TaskRepositoryImpl implements CachedTaskRepository {
      // ...
    
      @InvalidateAssignCache(namespace="Taskrepo", assignedKey="all")
      public Task saveAndClearCache(Task t){
        return this.taskRepository.save(t);
      }
    
      @InvalidateAssignCache(namespace="Taskrepo", assignedKey="all")
      public void deleteByIdAndClearCache(Long id){
        this.taskRepository.deleteById(id);
      }
    }

    The stale data is invalidated here via @InvalidateAssignCache annotation. Just as @ReadThroughAssignCache it acts on the assigned key that is declared in the annotation.

Now we can use these wrapper functions in our controller to clear the cache whenever a request comes in to add or delete a task. To do so replace save and deleteById in src/main/java/com/memcachier/tutorial/TaskController.java with saveAndClearCache and deleteByIdAndClearCache like so:

// ...
@RequestMapping(method = RequestMethod.POST)
public String newTask(ModelMap model,
                      @ModelAttribute("newTask") @Valid Task task,
                      BindingResult result) {
  if (!result.hasErrors()) {
    this.taskRepo.saveAndClearCache(task);
  }
  return showAllTasks(model);
}

@RequestMapping(method = RequestMethod.DELETE)
public String deleteTask(ModelMap model, @RequestParam("taskId") Long id) {
  this.taskRepo.deleteByIdAndClearCache(id);
  return showAllTasks(model);
}

Deploy the fixed task list:

$ mvn package -Dmaven.test.skip=true
$ cf push

Add a new task and you will see all the tasks appear you have added since we implemented caching for the task list.

Further reading & resources