Complete tutorial of building Spring REST API with Hibernate using Second Level Cache showing Hit and Miss.

Configuration
Java Compilation:
Java Runtime:
Web Server:
Database:
Maven:
Java Compilation:
Java Runtime:
Web Server:
Database:
Gradle:
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
MySQL 8.0.35
Maven 3.9.6
JDK 17.0.2
JRE OpenJDK 17.0.2
Tomcat 10.1.16
MySQL 8.0.35
Gradle 8.6
Create the following folder structure:
folder structure of Spring REST API Hibernate with Second Level Cache Hit and Miss
pom.xml: (if using Gradle, replace pom.xml with build.gradle)
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.maxjava</groupId> <artifactId>SpringRESTAPIHibernateSecondLevelCache</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>6.5.2.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jcache</artifactId> <version>6.5.2.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-c3p0</artifactId> <version>6.5.2.Final</version> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.9.11</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'war' } java { sourceCompatibility = 17 targetCompatibility = 17 } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.springframework', name: 'spring-webmvc', version: '6.1.6' implementation group: 'org.springframework', name: 'spring-orm', version: '6.1.6' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.4' implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.33' implementation group: 'org.hibernate', name: 'hibernate-core', version: '6.5.2.Final' implementation group: 'org.hibernate', name: 'hibernate-c3p0', version: '6.5.2.Final' implementation group: 'org.hibernate', name: 'hibernate-jcache', version: '6.5.2.Final' implementation group: 'org.ehcache', name: 'ehcache', version: '3.9.11' compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.30' }
Some notes about pom.xml:
  • spring-webmvc for necessary Spring REST controllers and its dependencies
  • spring-orm for Hibernate session related classes like session factories
  • jackson-databind is for the java object serialization and deserialization
  • hibernate-core for Hibernate classes
  • hibernate-c3p0 is for datasource
  • hibernate-jcache and ehcache for second level cache
  • lombok to remove boilerplate code like get and set
  • maven-compiler-plugin is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
Some notes about build.gradle:
  • spring-webmvc for necessary Spring REST controllers and its dependencies
  • spring-orm for Hibernate session related classes like session factories
  • jackson-databind is for the java object serialization and deserialization
  • hibernate-core for Hibernate classes
  • hibernate-c3p0 is for datasource
  • hibernate-jcache and ehcache for second level cache
  • lombok to remove boilerplate code like get and set
  • sourceCompatibility and targetCompatibility is for compiling the code using source version 17 and target version 17
  • A war artifact will be created for deployment to servlet containers
web.xml:
<web-app> <display-name>Spring REST API with Hibernate using Second Level Cache</display-name> <servlet> <servlet-name>SpringRESTAPIHibernateSecondLevelCache</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/myApplicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringRESTAPIHibernateSecondLevelCache</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Some notes about web.xml:
  • org.springframework.web.servlet.DispatcherServlet is the servlet class for Spring
  • /WEB-INF/myApplicationContext.xml is the location of the Spring context configuration file
myApplicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="controller,repository" /> <tx:annotation-driven transaction-manager="transactionManager"/> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.cj.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mydb"/> <property name="user" value="myuser"/> <property name="password" value="mypassword"/> <property name="initialPoolSize" value="10"/> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:hibernate.cfg.xml"/> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> </beans>
Some notes about myApplicationContext.xml:
  • <context:component-scan base-package="controller,repository"/> is for Spring to look for classes in controller,repository packages to create beans
  • <tx:annotation-driven transaction-manager="transactionManager"/> is to allow Spring to use annotation for transaction management, and the transaction-manager is the bean org.springframework.orm.hibernate5.HibernateTransactionManager defined below
  • <mvc:message-converters> is to specify library for json serialization and deserialization
  • <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> is to specify the datasource for use in LocalSessionFactoryBean
  • <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> is to create LocalSessionFactoryBean for autowiring in the code to create Hibernate Session
customer.hbm.xml:
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="repository.CustomerEntity" table="customer"> <id name="name" type="string"> <column name="Name"/> </id> <property name="email" type="string"> <column name="Email"/> </property> <property name="createdDate" type="timestamp"> <column name="CreatedDate"/> </property> </class> </hibernate-mapping>
Some notes about customer.hbm.xml:
  • The mapping is based on the following table schema:
    CREATE TABLE customer ( Name VARCHAR(50) NOT NULL, Email VARCHAR(50) NULL DEFAULT NULL, CreatedDate TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (Name) )
hibernate.cfg.xml:
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="show_sql">true</property> <property name="hibernate.javax.cache.missing_cache_strategy">create</property> <property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property> <property name="hibernate.cache.use_second_level_cache">true</property> <property name="hibernate.generate_statistics">true</property> <mapping resource="customer.hbm.xml"/> <class-cache class="repository.CustomerEntity" usage="read-write"/> </session-factory> </hibernate-configuration>
Some notes about hibernate.cfg.xml:
  • Setting show_sql to true allow printing of sql statement every time there is sql call to mysql, this is for debugging second level cache is used. After debugging, this can be turned off
  • Make sure second level cache is turned on by setting "hibernate.cache.use_second_level_cache" to true. In addition, need to specify which class is using the cache by using class-cache, with usage option like read-write
  • Specify "org.ehcache.jsr107.EhcacheCachingProvider" as the "hibernate.javax.cache.provider"
  • Specify the Hibernate mapping file in mapping resource
  • Setting "generate_statistics" to true
MyController.java:
package controller; import model.Stat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import model.Customer; import repository.CustomerRepository; @RestController @RequestMapping("/myapp") public class MyController { @Autowired private CustomerRepository customerRepository; @GetMapping("/stat") public Stat getStat() { return customerRepository.getStat(); } @GetMapping("/customer") public Customer getCustomer(@RequestParam("name") String name) { Customer customer = customerRepository.getCustomerByName(name); return customer; } @PostMapping("/customer") public void postCustomer(@RequestBody Customer customer) { customerRepository.saveCustomer(customer); } }
Some notes about MyController.java:
  • This is the Spring REST web service which putting @RestController will do the trick (and make sure the package of this class got scanned in bean creation)
  • Autowires CustomerRepository which is the bean to interact with the database
  • Provides basic Get and Post REST APIs which is enough to test the second level cache hit and miss
Customer.java:
package model; import lombok.Data; @Data public class Customer { private String name; private String email; }
Some notes about Customer.java:
  • This is the object used in the request body and response body of the REST APIs for inserting and deleting Customer
Stat.java:
package model; import lombok.Data; @Data public class Stat { private long secondLevelCacheHitCount; private long secondLevelCacheMissCount; }
Some notes about Stat.java:
  • This is response body of the REST APIs for showing second level cache hit and miss information
CustomerEntity.java:
package repository; import lombok.Data; import java.util.Date; @Data public class CustomerEntity { private String name; private String email; private Date createdDate; }
Some notes about CustomerEntity.java:
  • This is mapped class for table "Customer"
CustomerRepository.java:
package repository; import model.Customer; import model.Stat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.*; import org.hibernate.SessionFactory; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Repository public class CustomerRepository { @Autowired private SessionFactory sessionFactory; public Stat getStat() { long cacheHit = sessionFactory.getStatistics().getSecondLevelCacheHitCount(); long cacheMiss = sessionFactory.getStatistics().getSecondLevelCacheMissCount(); Stat stat = new Stat(); stat.setSecondLevelCacheHitCount(cacheHit); stat.setSecondLevelCacheMissCount(cacheMiss); return stat; } @Transactional public Customer getCustomerByName(String name) { CustomerEntity customerEntity = sessionFactory.getCurrentSession().get(CustomerEntity.class, name); Customer customer = new Customer(); if (customerEntity != null) { customer.setEmail(customerEntity.getEmail()); customer.setName(customerEntity.getName()); } return customer; } @Transactional public void saveCustomer(Customer customer) { CustomerEntity customerEntity = new CustomerEntity(); customerEntity.setName(customer.getName()); customerEntity.setEmail(customer.getEmail()); customerEntity.setCreatedDate(new Date()); sessionFactory.getCurrentSession().save(customerEntity); } }
Some notes about CustomerRepository.java:
  • @Repository to specify the class as Spring bean
  • Autowires SessionFactory bean which is defined in the myApplicationContext.xml file
  • Put @Transactional around each Hibernate function, even for get, as transaction is needed for get for Hibernate
Sample requests and responses to test out second level cache hit and miss(assume the war file is deployed to <CONTEXT_PATH>):
  • First getting the stat:
    curl --request GET '<CONTEXT_PATH>/myapp/stat'
    Response:
    { "secondLevelCacheHitCount": 0, "secondLevelCacheMissCount": 0 }
    At first there is no hit or miss
     
  • Retrieve a non-existing record:
    curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'
    No record since there is no customer for this name
     
  • Getting the stat again:
    curl --request GET '<CONTEXT_PATH>/myapp/stat'
    Response:
    { "secondLevelCacheHitCount": 0, "secondLevelCacheMissCount": 1 }
    There is a miss
     
  • Add a new record:
    curl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "name": "Ethan", "email": "ethan@maxjava.net" }'
  • Retrieve the same record again:
    curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'
    Response:
    { "name": "Ethan", "email": "ethan@maxjava.net" }
    This time the record is returned
     
  • Getting the stat again:
    curl --request GET '<CONTEXT_PATH>/myapp/stat'
    Response:
    { "secondLevelCacheHitCount": 1, "secondLevelCacheMissCount": 1 }
    There is a hit now, in addition to a miss