Complete tutorial of building Spring REST API application with Hibernate and JPA using 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 and JPA 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>SpringRESTAPIHibernateJpaSecondLevelCache</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 gradle.build:
  • 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.show_sql" 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 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
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 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 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 Customer getCustomerByName(String name) { CustomerEntity customerEntity = entityManager.find(CustomerEntity.class, name); Customer customer = new Customer(); customer.setName(customerEntity.getName()); customer.setEmail(customerEntity.getEmail()); 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
  • @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(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 (Email, CreatedDate, 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 customer0_.Name as Name1_0_0_, customer0_.Email as Email2_0_0_, customer0_.CreatedDate as CreatedD3_0_0_ from customer customer0_ where customer0_.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