Complete tutorial of building Spring REST API with Hibernate and JPA 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 and JPA with Second Level Cache showing 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>SpringRESTAPIHibernateJpaSecondLevelCacheHitMiss</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' } sourceCompatibility = 11 targetCompatibility = 11 group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.1.6.RELEASE' implementation group: 'org.springframework', name: 'spring-tx', version: '5.1.6.RELEASE' implementation group: 'org.springframework', name: 'spring-orm', version: '5.1.6.RELEASE' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9' implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.29' implementation group: 'org.hibernate', name: 'hibernate-core', version: '5.3.10.Final' implementation group: 'org.hibernate', name: 'hibernate-c3p0', version: '5.3.10.Final' implementation group: 'org.hibernate', name: 'hibernate-jcache', version: '5.3.10.Final' implementation group: 'org.ehcache', name: 'ehcache', version: '3.8.1' implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.1' compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.2' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.2' }
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 and JPA with Second Level Cache</display-name> <servlet> <servlet-name>SpringRESTAPIHibernateJpaSecondLevelCache</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>SpringRESTAPIHibernateJpaSecondLevelCache</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="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </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.jpa.JpaTransactionManager defined below
  • <mvc:message-converters> is to specify library for json serialization and deserialization
  • <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"/> is to create Spring managed EntityManagerFactory, which is used for create EntityManager for Jpa operations
  • <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> is to create a transaction manager bean for transactional database interaction like insert or delete
persistence.xml:
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"> <persistence-unit name="myPersistenceUnit"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <class>repository.CustomerEntity</class> <properties> <property name="hibernate.connection.driver_class" value="com.mysql.cj.jdbc.Driver"/> <property name="hibernate.connection.username" value="myuser"/> <property name="hibernate.connection.password" value="mypassword"/> <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/mydb"/> <property name="hibernate.c3p0.max_size" value="50"/> <property name="hibernate.c3p0.timeout" value="300"/> <property name="hibernate.cache.use_second_level_cache" value="true"/> <property name="hibernate.javax.cache.missing_cache_strategy" value="create"/> <property name="hibernate.javax.cache.provider" value="org.ehcache.jsr107.EhcacheCachingProvider"/> <property name="hibernate.generate_statistics" value="true"/> </properties> </persistence-unit> </persistence>
Some notes about persistence.xml:
  • The name myPersistenceUnit is used in the code to reference this particular persistence-unit
  • Need to define all the mapped class in the class tag. repository.CustomerEntity is the mapped class in this example, so defined here
  • Connection configurations like username, password, c3p0 pool size etc... are all defined here
  • Make sure second level cache is turned on by setting "hibernate.cache.use_second_level_cache" to true
  • Specify "org.ehcache.jsr107.EhcacheCachingProvider" as the "hibernate.javax.cache.provider"
  • 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 org.hibernate.annotations.CacheConcurrencyStrategy; import jakarta.persistence.*; import java.util.Date; @Data @Entity @Table(name="Customer") @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class CustomerEntity { @Id @Column(name="Name", length=50, nullable=false) private String name; @Basic @Column(name="Email") private String email; @Temporal(TemporalType.TIMESTAMP) @Column(name="CreatedDate") private Date createdDate; }
Some notes about CustomerEntity.java:
  • This is mapped class for table "Customer"
  • @Entity to specify this class as an JPA entity, which is mandatory for mapping the class to a database table
  • @Table is optional but can be used to specify the name of the table
  • @Id is the primary key and @Column is the columns of the table
  • @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) to specify second level caching this entity. In general CacheConcurrencyStrategy.READ_WRITE should be good on most cases. However, it should depend on actual scenario to determine cache concurrency strategy
  • 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`) )
CustomerRepository.java:
package repository; import model.Customer; import model.Stat; import org.hibernate.SessionFactory; import org.springframework.stereotype.*; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.util.Date; @Repository public class CustomerRepository { @PersistenceContext(name="myPersistenceUnit") public EntityManager entityManager; public Stat getStat() { SessionFactory sessionFactory = entityManager.getEntityManagerFactory().unwrap(SessionFactory.class); long cacheHit = sessionFactory.getStatistics().getSecondLevelCacheHitCount(); long cacheMiss = sessionFactory.getStatistics().getSecondLevelCacheMissCount(); Stat stat = new Stat(); stat.setSecondLevelCacheHitCount(cacheHit); stat.setSecondLevelCacheMissCount(cacheMiss); return stat; } public Customer getCustomerByName(String name) { CustomerEntity customerEntity = entityManager.find(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()); entityManager.persist(customerEntity); } }
Some notes about CustomerRepository.java:
  • @PersistenceContext to autowire the EntityManager identified by persistence unit defined in persistence.xml
  • Use the unwrap method of the EntityManagerFactory to get the Hibernate SessionFactory and then use the getStatistics method of the Hibernate SessionFactory to get the statistics
  • @Transactional is needed for operations required transaction like persist
  • Mark this with @Repository as a bean
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 response body, 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