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:
- Familiarity with Java (and ideally Spring Boot)
- A Pivotal Web Services user account (signup is free and instant)
- Maven and the Cloud Foundry CLI installed on your computer
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:
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
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:
- Set up the database
- Create a
Task
entity and a table to store them - 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 inVCAP_SERVICES
except that it starts withjdbc:postgresql
instead ofpostgres
. 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.
Add the
Task
entity tosrc/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); } }
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 aJpaRepository
instead. See this StackOverflow thread for more information.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
andchangelog
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(); .addAttribute("tasks", tasks); model.addAttribute("newTask", new Task()); modelreturn "task"; } @RequestMapping(method = RequestMethod.POST) public String newTask(ModelMap model, @ModelAttribute("newTask") @Valid Task task, ) { BindingResult resultif (!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 thetask
view, toPOST
a new task that will then be saved to the database, and toDELETE
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
= new JSONObject(vcap_services);
JSONObject root = root.getJSONArray("memcachier");
JSONArray mc = mc.getJSONObject(0).getJSONObject("credentials");
JSONObject credentials = credentials.getString("servers");
memcachier_servers = credentials.getString("username");
memcachier_username = credentials.getString("password");
memcachier_password }
String serverString = memcachier_servers.replace(",", " ");
List<InetSocketAddress> servers = AddrUtil.getAddresses(serverString);
= AuthInfo.plain(memcachier_username,
AuthInfo authInfo );
memcachier_passwordMap<InetSocketAddress, AuthInfo> authInfoMap =
new HashMap<InetSocketAddress, AuthInfo>();
for(InetSocketAddress server : servers) {
.put(server, authInfo);
authInfoMap}
final XMemcachedConfiguration conf = new XMemcachedConfiguration();
.setUseBinaryProtocol(true);
conf.setAuthInfoMap(authInfoMap);
conf
final CacheFactory cf = new CacheFactory();
.setCacheClientFactory(new MemcacheClientFactoryImpl());
cf.setAddressProvider(new DefaultAddressProvider(serverString));
cf.setConfiguration(conf);
cfreturn 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:
Build an interface with the methods that should be added to the
TaskRepository
insrc/main/java/com/memcachier/tutorial/CachedTaskRepository.java
:package com.memcachier.tutorial; import java.lang.Iterable; public interface CachedTaskRepository { public Iterable<Task> findAllCached(); }
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.Make sure this implementation is integrated into the
TaskRepository
. This is simply done by making theTaskRepository
interface also extend theCachedTaskRepository
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:
Declare the wrapper methods in the
CachedTaskRepository
interface insrc/main/java/com/memcachier/tutorial/CachedTaskRepository.java
:// ... public interface CachedTaskRepository { public Iterable<Task> findAllCached(); public Task saveAndClearCache(Task t); public void deleteByIdAndClearCache(Long id); }
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 resultif (!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.