Complete tutorial of building Spring REST API with Hibernate Second Level Cache.

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
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 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> <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
MyController.java:
package controller; 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("/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
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
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 Customer.java:
  • This is mapped class for table 'Customer'
CustomerRepository.java:
package repository; import model.Customer; 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; @Transactional public Customer getCustomerByName(String name) { CustomerEntity customerEntity = sessionFactory.getCurrentSession().get(CustomerEntity.class, name); Customer customer = new Customer(); 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(assume the war file is deployed to <CONTEXT_PATH>):
  • Add a new record:
    curl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "name": "Ethan", "email": "ethan@maxjava.net" }'
    Console print out:
    Hibernate: insert into customer (CreatedDate,Email,Name) values (?,?,?)
    Hibernate is calling MySQL to insert the record
     
  • Retrieve the record:
    curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'
    Response:
    { "name": "Ethan", "email": "ethan@maxjava.net" }
    Console print out nothing: so meaning Hibernate is getting the record from the second level cache
     
  • Now restart the web server, retrieve the same record:
    curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'
    Response:
    { "name": "Ethan", "email": "ethan@maxjava.net" }
    Console print out this time:
    Hibernate: select ce1_0.Name,ce1_0.CreatedDate,ce1_0.Email from customer ce1_0 where ce1_0.Name=?
    Since there is no Customer with key Ethan in the second level cache, the call to MySQL is made
     
  • Call again:
    curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'
    Response:
    { "name": "Ethan", "email": "ethan@maxjava.net" }
    Console print out nothing: since Hibernate is getting the record from second level cache this time