Introduction

Here we are going to see an example on implementing caching in Spring REST services. Caching is used to store copies of frequently accessed data in several places for the request-response path. In other words, it stores a copy of a given resource and serves it back when requested.

The performance of web sites and applications can be significantly improved by reusing previously fetched resources. Caching in REST services reduce latency and network traffic and thus lessen the time needed to display a representation of a resource. By making use of HTTP caching, REST APIs become more responsive.

There are several kinds of caches but these can be grouped into two main categories – private or shared caches. A shared cache is a cache that stores responses for reuse by more than one user. A private cache is dedicated to a single user.

If you want to know more on caching in REST services then you can find in Google search.

Prerequisites

Eclipse Neon, Java 1.8, Spring Boot 2.1.6, Gradle 5.4.1

Go through the below steps to implement caching in Spring REST services.

Creating Project

Create a gradle based project in Eclipse, the project name you can give as spring-boot-rest-caching.

Updating Build Script

As soon as project creation is done in eclipse, update the build.gradle file with the below content to include required dependencies.

In the below build script we have included spring boot web, spring boot data jpa and h2 database. The in-memory h2 database is very useful when you need to do a quick PoC because it saves you a lots of time and effort required to setup a database.

buildscript {
	ext {
		springBootVersion = '2.1.6.RELEASE'
	}
    repositories {
    	mavenLocal()
    	mavenCentral()
    }
    dependencies {
    	classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
    
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenLocal()
    mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-cache:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
	runtime("com.h2database:h2:1.4.196")
}

Creating Entity Class

We will work with database, where we will perform database operations. Therefore we need to create entity class to map out database column with Java class attributes.

The beauty of Spring Boot Data JPA starter is the below class will create the required table in the database. Even we didn’t mention any column name, so it will create same column name in the table as the Java attributes.

package com.jeejava.spring.boot.rest.caching.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String name;
	private String address;

	public User() {
	}

	public User(String name, String address) {
		this.name = name;
		this.address = address;
	}

	//getters and setters

}

Creating Repository Interface

Spring Data JPA provides built-in methods for performing basic CRUD operations and you don’t need to write any query method.

Another beauty of Spring Data JPA is that you write Java method to query your database and Spring generates the corresponding query from your method for performing database operations.

We have just extended the JpaRepository interface but we have just written one query method inside this repository to fetch user by name, but we will use Spring’s built-in methods for performing other operations.

package com.jeejava.spring.boot.rest.caching.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.jeejava.spring.boot.rest.caching.entity.User;

public interface UserRepository extends JpaRepository<User, Integer> {

	User findByName(String name);

}

Creating Service Class

The service class is used generally if you want to perform some business logic or conversion to different data.

We are fetching user data on different conditions in this service class.

package com.jeejava.spring.boot.rest.caching.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.jeejava.spring.boot.rest.caching.entity.User;
import com.jeejava.spring.boot.rest.caching.repository.UserRepository;

@Service
public class UserService {

	@Autowired
	private UserRepository repository;

	public List<User> getUserList() {
		return repository.findAll();
	}

	public User getUserById(int id) {
		return repository.findById(id).get();
	}

	public User getUserByName(String name) {
		return repository.findByName(name);
	}

}

Creating REST Controller

Now we will expose our REST API so that any client can call our REST service and retrieve user data.

Exposing the operations on REST makes loosely coupled to integrate with any client technologies.

package com.jeejava.spring.boot.rest.caching.rest.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.jeejava.spring.boot.rest.caching.entity.User;
import com.jeejava.spring.boot.rest.caching.service.UserService;

@RestController
public class UserRestController {

	@Autowired
	private UserService service;

	@GetMapping("/users")
	public ResponseEntity<List<User>> getUserList() {
		return new ResponseEntity<List<User>>(service.getUserList(), HttpStatus.OK);
	}

	@GetMapping("/user/id/{id}")
	public ResponseEntity<User> getUserById(@PathVariable int id) {
		return new ResponseEntity<User>(service.getUserById(id), HttpStatus.OK);
	}

	@GetMapping("/user/name/{name}")
	public ResponseEntity<User> getUserByName(@PathVariable String name) {
		return new ResponseEntity<User>(service.getUserByName(name), HttpStatus.OK);
	}

}

Creating Properties File

We will create an application.properties file that will hold some configurations. You can also create application.yml file. This file should be put under src/main/resources folder.

The below properties file contains SQL format and h2 database console enablement to see the queries executed during performing of the database operations.

spring.jpa.show-sql=true
spring.h2.console.enabled=true

Dumping Data into Table

As we are not creating new record through REST API and we need some data to test our application, so we will insert some data during application startup.

Therefore create below SQL insert script and put it under src/main/resources folder.

insert into user(name,address)
values('Soumitra','Earth');
insert into user(name,address)
values('Liton','Mars');
insert into user(name,address)
values('Suman','Jupiter');
insert into user(name,address)
values('Debabrata','Neptune');

Creating Main Class

Creating a main would be sufficient to deploy our application into the Tomcat server. This is a great advantage that you just need to let Spring know that it is your Spring Boot Application using @SpringBootApplication and main class.

We also need to tell Spring where our entity class and repository interface.

package com.jeejava.spring.boot.rest.caching.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication(scanBasePackages = "com.jeejava.spring.boot.rest.caching")
@EntityScan("com.jeejava.spring.boot.rest.caching.entity")
@EnableJpaRepositories("com.jeejava.spring.boot.rest.caching.repository")
public class SpringRestCachingApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestCachingApp.class, args);
	}

}

Testing the Application

Now if you run the above main class and test your REST APIs then you will see every time you hit the REST URL, your data will be fetched from database. Therefore on every request through REST API database gets hit.

For example, you may try these below requests:

Fetching All Users

Request – GET http://localhost:8080/users 2 times

Response

[
    {
        "id": 1,
        "name": "Soumitra",
        "address": "Earth"
    },
    {
        "id": 2,
        "name": "Liton",
        "address": "Mars"
    },
    {
        "id": 3,
        "name": "Suman",
        "address": "Jupiter"
    },
    {
        "id": 4,
        "name": "Debabrata",
        "address": "Neptune"
    }
]

Console Output

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_

Fetching User by ID

Request – GET http://localhost:8080/user/id/1 3 times

Response

{
	"id": 1,
	"name": "Soumitra",
	"address": "Earth"
}

Console Output

Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?

Fetching User by Name

Request – GET http://localhost:8080/user/name/Soumitra 2 times

Response

{
    "id": 1,
    "name": "Soumitra",
    "address": "Earth"
}

Console Output

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?

So we have seen our example without using caching in Spring REST services and that’s why every time we request the REST API the database was called.

Now we will implement caching in REST services and we will see no matter how many times we call the REST API the database will be hit only first time to fetch data from database and put into cache. Next time onward for each same request data will be fetch from cache.

Implementing Caching

Now we will implement caching in Spring REST services. So we will update our existing service class and main class we had created in the above sections.

Updating Service Class

Update the service class as follows to implement caching for our public methods.

We need to put @Cacheable annotation with name so that Spring will find data into cache easily by cache name.

package com.jeejava.spring.boot.rest.caching.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.jeejava.spring.boot.rest.caching.entity.User;
import com.jeejava.spring.boot.rest.caching.repository.UserRepository;

@Service
public class UserService {

	@Autowired
	private UserRepository repository;

	@Cacheable("users")
	public List<User> getUserList() {
		return repository.findAll();
	}

	@Cacheable("user-id")
	public User getUserById(int id) {
		return repository.findById(id).get();
	}

	@Cacheable("user-name")
	public User getUserByName(String name) {
		return repository.findByName(name);
	}

}

Updating Main Class

Update the main class to enable the processing of the caching annotations.

The @EnableCaching annotation triggers a post processor that inspects every Spring bean for the presence of caching annotations on public methods. If such an annotation is found, a proxy is automatically created to intercept the method call and handle the caching behavior accordingly.

package com.jeejava.spring.boot.rest.caching.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableCaching
@SpringBootApplication(scanBasePackages = "com.jeejava.spring.boot.rest.caching")
@EntityScan("com.jeejava.spring.boot.rest.caching.entity")
@EnableJpaRepositories("com.jeejava.spring.boot.rest.caching.repository")
public class SpringRestCachingApp {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestCachingApp.class, args);
	}

}

Testing the Application

Now call the REST APIs to fetch data from database. You will find only first time database is hit and for subsequent requests the data are fetched from caching no matter how many times you invoke the REST services.

You will find only one database hit per each REST service call in the console output.

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_
Hibernate: select user0_.id as id1_0_0_, user0_.address as address2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?

Source Code

You can download source code.

Thanks for reading.

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *