Dropwizard Guava Cache Tutorial

Dropwizard Guava Cache Tutorial

Introduction

         I am assuming that you have a Basic Knowledge of Dropwizard and have a Basic Dropwizard Application running in your machine. If not, please check my blog on Basic Dropwizard Application by going to the link: Dropwizard Tutorial

        Well firstly why is caching needed in any application? There may be operations in your application that access data from a database or data from some other application. This data may not change frequently or it may be static data. Calling any other service for data is a very costly operation and may slow down your application. Memory savings will result in an optimized and quick application. For any such frequent data fetch operations we can create a basic Key Value based cache in the application. This cache gets populated when the data fetch service is called the first time. All subsequent calls will fetch the same data from the populated cache instead of the data source.

        Google has created Project Guava which are Core Libraries for Java. This project can be found on GitHub. Caching is part of this project. Guava Cache is an in-memory cache. In this tutorial, we will be integrating a Dropwizard Application with Guava Cache. Guava is automatically added to your project when dropwizard-core dependency is added to your project pom,xml.

Requirements to Run the Application
  1. Java
  2. Maven
  3. IDE of your choice
Once you have a Basic Dropwizard Application, here are the additional steps required to add the Guava Cache integration.


Step 1: Student POJO and CacheConfigManager Class

        In this post I am using a basic Student POJO as an example. Student has 3 attributes: name, universityId and subjectSpecialization

Here is Student.java:

package com.aj.dropwizardcache.domain;

public class Student {

    private String name;
    private String universityId;
    private String subjectSpecialization;

    public Student(String name, String universityId, String subjectSpecialization) {
        this.name = name;
        this.universityId = universityId;
        this.subjectSpecialization = subjectSpecialization;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUniversityId() {
        return universityId;
    }

    public void setUniversityId(String universityId) {
        this.universityId = universityId;
    }

    public String getSubjectSpecialization() {
        return subjectSpecialization;
    }

    public void setSubjectSpecialization(String subjectSpecialization) {
        this.subjectSpecialization = subjectSpecialization;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", universityId='" + universityId + '\'' +
                ", subjectSpecialization='" + subjectSpecialization + '\'' +
                '}';
    }
}


        In this class, I am building a new Student Cache. Here are a few values that I have added while building this cache:
  • concurrencyLevel: Specifies the concurrency level to be supported by the cache
  • maximumSize: Maximum number of records the cache can hold
  • expireAfterAccess: Time after which the cache will expire or will be cleared
  • recordStats: Provision to record Cache statistics like hitcount, missCount, loadSuccessCount, totalLoadTime....
While building the new cache we have to Override the load method and specify the method from where the data should be populated if it does not exist in the cache.

In this class I am also defining a method which returns student data when a key is provided. Observe how I have obtained CacheStats and logged it. 

Here is the class: CacheConfigManager.java

package com.aj.dropwizardcache.cache;

import com.aj.dropwizardcache.domain.Student;
import com.aj.dropwizardcache.service.StudentService;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class CacheConfigManager {

    private static final Logger logger = LoggerFactory.getLogger(CacheConfigManager.class);

    private static CacheConfigManager cacheConfigManager = new CacheConfigManager();

    public static CacheConfigManager getInstance() {
        return cacheConfigManager;
    }

    private static LoadingCache<String, Student> studentCache;

    public void initStudentCache(StudentService studentService) {
        if (studentCache == null) {
            studentCache =
                    CacheBuilder.newBuilder()
                            .concurrencyLevel(10)
                            .maximumSize(200) // Maximum of 200 records can be cached
                            .expireAfterAccess(30, TimeUnit.MINUTES) // Cache will expire after 30 minutes
                            .recordStats()
                            .build(new CacheLoader<String, Student>() { // Build the CacheLoader

                                @Override
                                public Student load(String universityId) throws Exception {
                                    logger.info("Fetching Student Data from DB/ Cache Miss");
                                    return studentService.getFromDatabase(universityId);
                                }
                            });
        }
    }

    public Student getStudentDataFromCache(String key) {
        try {
            CacheStats cacheStats = studentCache.stats();
            logger.info("CacheStats = {} ", cacheStats);
            return studentCache.get(key);
        } catch (ExecutionException e) {
            logger.error("Error Retrieving Elements from the Student Cache"
                    + e.getMessage());
        }
        return null;
    }

}

Step 2: Student Service Class

        In this post I am not using a database instead I am creating and using a HashMap as my database. This HashMap has universityId as key and the Student Object as Value. In this class I have created a method getFromDatabase which will be called whenever the cache does not have the key that I am looking for.

Here is StudentService.java:

package com.aj.dropwizardcache.service;

import com.aj.dropwizardcache.domain.Student;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

public class StudentService {

    private static final Logger logger = LoggerFactory.getLogger(StudentService.class);

    public StudentService() {
    }

    public static Student getFromDatabase(String universityId) {
        Student student1 = new Student("Jim", "S100", "Science");
        Student student2 = new Student("Steve", "M101", "Maths");
        Student student3 = new Student("Mark", "P102", "Physics");
        Map<String, Student> database = new HashMap<>();
        database.put("S100", student1);
        database.put("M101", student2);
        database.put("P102", student3);
        logger.info("Database called for: {}", universityId);
        return database.get(universityId);
    }
}

Step 3: API to test Cache

        I have created a GET API in Dropwizard which will call method to get student data from cache. I have written code to call this method 3 times. So during the invocation, if you check the application logs, it will clearly show that the data is retrieved from the database during the first call. During the first call the cache will also get populated. Hence the subsequent calls will retrieve data from the Cache.

Here is CacheResource.java:

package com.aj.dropwizardcache.resource;

import com.aj.dropwizardcache.cache.CacheConfigManager;
import com.codahale.metrics.annotation.Timed;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;

@Path("/cache")
@Produces(MediaType.APPLICATION_JSON)
@Api(value = "cache", description = "Cache Resource for getting student data from DB/cache")
public class CacheResource {

    private static final Logger logger = LoggerFactory.getLogger(CacheResource.class);

    @Timed
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "cache")
    public Response cache() {
        logger.info("In CacheResource.cache()...Get Student Data");
        //On the first call, data will be fetched from DB and
        //cache will be populated with the corresponding student record
        //On all subsequent calls, data will be returned from the cache
        for (int i = 1; i < 4; i++) {
            getStudentData(i);
        }
        Map<String, String> response = new HashMap<>();
        response.put("message", "Student Data has been retrieved");
        return Response.ok(response).build();
    }

    private void getStudentData(int i) {
        logger.info("********** Call " + String.valueOf(i) + " Started **********");
        logger.info("Call " + String.valueOf(i) + ": {}", CacheConfigManager.getInstance().getStudentDataFromCache("S100"));
        logger.info("Call " + String.valueOf(i) + ": {}", CacheConfigManager.getInstance().getStudentDataFromCache("M101"));
        logger.info("Call " + String.valueOf(i) + ": {}", CacheConfigManager.getInstance().getStudentDataFromCache("P102"));
        logger.info("********** Call " + String.valueOf(i) + " Ended **********");
    }
}

Step 4: Changes in Application class 

        This class is the entry point to the Dropwizard Application. In the Application class, I have initialized the Student Cache and registered the CacheResource

Here is DropwizardCacheApplication.java:
package com.aj.dropwizardcache;

import com.aj.dropwizardcache.resource.CacheResource;
import com.aj.dropwizardcache.resource.DropwizardCacheHealthCheckResource;
import com.aj.dropwizardcache.resource.PingResource;
import com.aj.dropwizardcache.cache.CacheConfigManager;
import com.aj.dropwizardcache.service.StudentService;
import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class DropwizardCacheApplication extends Application<DropwizardCacheConfiguration> {

    private static final Logger logger = LoggerFactory.getLogger(DropwizardCacheApplication.class);

 public static void main(String[] args) throws Exception {
  new DropwizardCacheApplication().run("server", args[0]);
 }

    @Override
    public void initialize(Bootstrap<DropwizardCacheConfiguration> b) {
    }

 @Override
 public void run(DropwizardCacheConfiguration config, Environment env)
   throws Exception {
        CacheConfigManager cacheConfigManager = CacheConfigManager
                .getInstance();
     StudentService studentService = new StudentService();
        cacheConfigManager.initStudentCache(studentService);
     logger.info("Registering RESTful API resources");
  env.jersey().register(new PingResource());
        env.jersey().register(new CacheResource());
  env.healthChecks().register("DropwizardCacheHealthCheck",
    new DropwizardCacheHealthCheckResource(config));
 }
}

Project Setup:

For Java Setup, please refer to:  Java Setup
For Maven Setup, please refer to: Maven Setup
For Git and Project Setup, please refer to: Git and Project Setup

Please Note: In this project, I have used yml file (dropwizardcache.yml) for basic configurations and I have set the server port as 4000 in this file. Hence server will run on port 4000.
This yml file can be found at: YML File

Run Application:

1. To run application in your IDE use:
    Program arguments: src/main/resources/dropwizardcache.yml



2. To run JAR from command prompt:
     Build jar by using command:
     mvn clean install
     Run JAR by using command in Project folder location:
     java -jar target\DropwizardCache-1.0.0.jar src/main/resources/dropwizardcache.yml

API calls and results:

1. GET API to test cache
 
 

        When this API is called, if you check the logs, you can see that in the first call, as data is not present in cache, it is retrieved from the Database (HashMap). Also the cache gets populated with the data in Call 1. So in Call 2 and Call 3 you can see that data is being retrieved from the cache.
        Also note how the Cache Stats are being printed in the logs. These stats give us a lot of data regarding the cache, its functioning and performance.



Other than this API, this application has the following APIs:

1. GET API for Application Health Check:
http://localhost:4000/admin/healthcheck

2. GET API for Ping:
http://localhost:4000/ping

3. POST API for Ping:
http://localhost:4000/ping
    JSON Request Body:
{
  "input": "ping"
}

Conclusion and GitHub link:

    In this post I have shown you how you can integrate your existing Dropwizard Application with Guava cache. Go ahead and give your application the power of caching and see the performance being improved and optimized. The code used in this post is available on GitHub.
    Learn the most popular and trending technologies like Machine Learning, Angular 5, Internet of Things (IoT), Akka HTTP, Play Framework, Dropwizard, Docker, Elastic Stack, Spring Boot and Flask in simple steps by reading my most popular blog posts at Software Developer Central.
    If you like my post, please feel free to share it using the share button just below this paragraph or next to the heading of the post. You can also tweet with #SoftwareDeveloperCentral on Twitter. To get a notification on my latest posts or to keep the conversation going, you can follow me on Twitter. Please leave a note below if you have any questions or comments.




Comments

Popular Posts

Golang gRPC Microservice

Dropwizard MySQL Integration Tutorial

Asynchronous Processing (@Async) in Spring Boot