Spring Boot HazelCast Example

Caching is one of the good techniques we can use to improve the performance of the applications. Spring provides good support for caching by providing different annotations out of the box. In this post, we will learn how to add HazelCast caching to our Spring boot application.

HazelCast is an open-source in-memory data grid. We can also use it for caching to improve the overall performance of the application.

We will create RESTful endpoints to perform CRUD operation on an Employee entity class and implement caching with the HazelCast spring library.

Version details:

  • Java version 1.8
  • Spring boot version 2.2.4.RELEASE
  • Spring boot JPA, Lombok and HazelCast libraries.
  • PostgreSQL database.

Let us begin.

Table of Contents

Create a Spring boot application

Create a Spring boot application. Add spring-boot-starter-web, spring-boot-starter-data-jpa, lombok dependencies while creating the project.

We need to add HazelCast dependencies, as highlighted in the below pom.xml file.

We need to add PostgreSQL maven dependency also, as we are using the PostgreSQL database in this example.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.asb.example</groupId>
    <artifactId>spring-boot-hazelcast-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-hazelcast-example</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.hazelcast/hazelcast-spring -->
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast-spring</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.hazelcast/hazelcast -->
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Configure the HazelCast caching

Create a configuration class and define a Spring bean called Config(), as shown below.

With this bean definition, we are configuring the HazelCast cache with a few of the supported configurations. We have defined configuration details like instance name, cache name, the maximum size of the cache, eviction policy, etc.

package com.asb.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import com.hazelcast.config.Config;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MaxSizeConfig;
import com.hazelcast.config.MaxSizeConfig.MaxSizePolicy;

@Configuration
public class HazelCastConfig {

    @Bean
    public Config config() {
        return new Config()
                .setInstanceName("hazelcast-instance")
                .addMapConfig(new MapConfig()
                        .setName("employeeCache")
                        .setMaxSizeConfig(new MaxSizeConfig()
                                .setSize(200)
                                .setMaxSizePolicy(MaxSizePolicy.FREE_HEAP_SIZE))
                        .setEvictionPolicy(EvictionPolicy.LRU)
                        .setTimeToLiveSeconds(300));
    }
}

To enable the caching support in the Spring boot application, we have to add the @EnableCaching annotation. Let us add the annotation to the Spring boot application class.

package com.asb.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class SpringBootHazelcastExampleApplication {

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

CRUD operation with JPA

Add the following application properties to connect the Spring boot application to the PostgreSQL database.

spring.datasource.url=jdbc:postgresql://localhost/postgres
spring.datasource.username=postgres
spring.datasource.password=asbnotebook

#Enable physcical naming strategy.
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true

We will perform the CRUD operation on the Employee entity. Create a table called Employee as shown below. We are using a database sequence called ‘employee_id_seq’ as an ID value.

CREATE SEQUENCE public.employee_id_seq INCREMENT 1 MINVALUE 1;

CREATE TABLE public.employee
(
  id bigint NOT NULL,
  designation character varying(255),
  employee_code character varying(255),
  employee_name character varying(255),
  CONSTRAINT employee_pkey PRIMARY KEY (id)
)

Create a JPA entity class and map it to the Employee table created in the earlier step.

package com.asb.example.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name="employee")
@Getter
@Setter
public class Employee {
    
    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="emp_seq")
    @SequenceGenerator(name = "emp_seq", sequenceName = "employee_id_seq", allocationSize=1)
    @Column(name = "id")
    private Long id;
    
    @Column(name="designation")
    private String designation;
    
    @Column(name="employee_code")
    private String employeeCode;
    
    @Column(name="employee_name")
    private String employeeName;
}

Create a JPA repository interface and extend the JpaRepository interface to make use of out of the box features of spring data JPA.

package com.asb.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.asb.example.model.Employee;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}

Enable the caching on CRUD operations

Create a service class called EmployeeService. Create the required methods to perform CRUD operations on the employee entity.

package com.asb.example.service;

import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.asb.example.model.Employee;
import com.asb.example.repository.EmployeeRepository;

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Cacheable(value = "employeeCache", key="#id")
    public Optional<Employee> getEmployeeDetails(Long id) {
        return employeeRepository.findById(id);
    }

    @CachePut(value = "employeeCache", key = "#employee.id", unless = "#result=null")
    public Employee addEmployeeDetails(Employee employee) {
        return employeeRepository.save(employee);
    }

    @CacheEvict(value = "employeeCache", key = "#id")
    public String deleteEmployeeDetails(Long id) {
        employeeRepository.deleteById(id);
        return "Employee with id:" + id + " deleted successfully";
    }

    @CachePut(key = "#employee.id", value = "employeeCache", unless = "#result=null")
    public Employee updateEmployeeDetails(Employee employee) {
        return employeeRepository.save(employee);
    }
}

We have used the @Caheable annotation to the getEmployeeDetails() method. This annotation adds the employee entity to the HazelCast cache if it is not present in the cache.

The next call to the method will return the cached entity from the cache instead of retrieving it from the database.

The other two annotations used are @CachePut and @CacheEvict.

We have used the @CachePut annotation to create and update methods. This annotation will add the entity into HazelCast cache upon creation or update of the employee entity.

We are also passing the cache key, the id field of the employee entity.

The ‘value’ parameter is the alias for the cache name, and the parameter ‘unless’ is used to specify conditional caching. We are caching the entity object only if the employee entity is not null.

We have used the @CacheEvict annotation to remove the cached object from the HazelCast cache upon the deletion of the entity from the database.

Create REST end points

Create a RESTful controller class called EmployeeController.java and add required RESTful endpoints.

package com.asb.example.controller;

import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.asb.example.model.Employee;
import com.asb.example.service.EmployeeService;

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/employees")
    public ResponseEntity<Employee> getEmployeeDetails(@RequestParam(name = "id") Long id) {
        Optional<Employee> emp = employeeService.getEmployeeDetails(id);
        return new ResponseEntity<>((emp.isPresent() ? emp.get() : null), HttpStatus.OK);
    }

    @PutMapping("/employees")
    public ResponseEntity<Employee> updateEmployeeDetails(@RequestBody Employee employee) {
        return new ResponseEntity<>(employeeService.updateEmployeeDetails(employee), HttpStatus.CREATED);
    }

    @PostMapping("/employees")
    public ResponseEntity<Employee> addEmployeeDetails(@RequestBody Employee employee) {
        return new ResponseEntity<>(employeeService.addEmployeeDetails(employee), HttpStatus.CREATED);
    }

    @DeleteMapping("/employees")
    public ResponseEntity<String> deleteEmployeeDetails(@RequestParam(name = "id") Long id) {
        return new ResponseEntity<>(employeeService.deleteEmployeeDetails(id), HttpStatus.OK);
    }
}

Time to run the application

Start the Spring boot application. Create a new employee object, as shown below.

Hazelcast cache put example

The JPA generated SQL insert statements are given below.

Hazelcast cache put example

Now retrieve the employee detail by passing the id.

Hazelcast cache put example

We can observe in the application console that the database call is not made to retrieve the employee record.

Also, the application is serving the Employee record from the cache, that got added while entity creation.

HazelCast cacheable example

Update the employee record, as shown below, using PUT operation.

HazelCast cacheable example

We can observe the SQL update statement generated by JPA.

HazelCast cachePut example

Retrieve the updated employee details by using the GET endpoint.

HazelCast CachePUT example

We can observe below that updated employee details are retrieved from the HazelCast cache.

The application is serving the employee object from the cache.

HazelCast Evict example

Finally, delete the employee object by passing the id.

HazelCast cacheEvict example

JPA generates the delete SQL statement to delete the employee object.

HazelCast cacheEvict example

Retrieve the deleted employee object by using the GET endpoint.

HazelCast cacheEvict example

We can also observe that SQL query is now generated by the JPA, as cache value is evicted from HazelCast upon the deletion of the employee entity object.

HazelCast cacheEvict example

Conclusion

In conclusion, we have learned how to configure the HazelCast cache in the Spring boot application.

We have also learned how to enable caching for CRUD operations to improve the performance of the application.

Example code is available on GitHub. Happy coding! 🙂

Spring Boot HazelCast Example
Scroll to top

Discover more from ASB Notebook

Subscribe now to keep reading and get access to the full archive.

Continue reading