Configuring Ehcache 3 with Hibernate 6 and 7: Setup Guide

Set up Ehcache 3 as the Hibernate 6 and 7 second level cache: the hibernate-jcache and jakarta Ehcache dependencies, the JCache properties, a minimal ehcache.xml, @Cacheable and @Cache, hit statistics, and the startup error each missing piece causes.

logo

Ehcache 3 is a cache library that Hibernate 6 and 7 use as a JCache provider for the second level cache, through the hibernate-jcache module. The module connects Hibernate’s second level cache to the standard JCache API, and Ehcache 3 stores the cached data.

The second level cache is shared by all sessions, so a lookup that one session already loaded skips the SELECT in the next session. We use it for lookup data that many requests read and few change, for example the airports that every flight search needs.

The following example sets the Hibernate properties for Ehcache 3, marks the Airport entity as cacheable and loads the same airport in two sessions.

.property("hibernate.cache.region.factory_class", "jcache")
.property("hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider")
.property("hibernate.javax.cache.uri", "ehcache.xml")
.property("hibernate.javax.cache.missing_cache_strategy", "fail")
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Airport { ... }
emf.runInTransaction(em -> em.find(Airport.class, "DEL"));   // select a1_0.iataCode,a1_0.city,a1_0.name from Airport a1_0 where a1_0.iataCode=?
emf.runInTransaction(em -> em.find(Airport.class, "DEL"));   // no SQL: L2 hits=1

Notice that the second session sends no SQL, because it finds Delhi airport in the Ehcache region.

The setup has four parts that we go through one by one, namely two Maven dependencies, a few Hibernate properties, an ehcache.xml file, and the @Cacheable and @Cache annotations on each entity we want cached. The same setup runs on Hibernate 6.6 and Hibernate 7.4.

1. How Hibernate Uses Ehcache 3

Hibernate does not call Ehcache itself, because since Hibernate 6 it uses Ehcache 3 only through JCache (JSR-107), the standard caching API in the javax.cache package. Three pieces work together.

  • The module hibernate-jcache contains JCacheRegionFactory, the class that asks the provider for one named cache, called a region, for each cached entity class, cached collection and query cache.
  • The JCache API (javax.cache:cache-api) is the interface between Hibernate and any cache provider.
  • Ehcache 3 implements that API with org.ehcache.jsr107.EhcacheCachingProvider and reads its cache definitions from ehcache.xml.

Once the pieces are in place, every em.find() checks three places in a fixed order. The first level cache belongs to one session, the Ehcache region is shared by all sessions, and the database comes last.

Lookup path for em.find(Airport.class, "DEL") in two sessions. Session 1: the first level cache misses, the Ehcache region com.howtodoinjava.hibernate.ehcache.Airport misses, Hibernate runs select ... from Airport where iataCode=? and puts the row into the region. Session 2: the first level cache misses again because it is a new session, the Ehcache region hits and no SQL runs
The first session runs the SELECT and fills the Ehcache region. The second session finds the airport there and sends nothing to the database.

Each piece of the setup has one job, and every missing piece except the annotations stops Hibernate at startup with its own error, as section 3 shows.

PieceWhereWhat it does
hibernate-jcachepom.xmlAdds the jcache region factory to Hibernate
ehcache with classifier jakartapom.xmlThe JCache provider, built for Jakarta EE 9+
hibernate.cache.region.factory_classHibernate propertiesSelects the jcache region factory
hibernate.javax.cache.provider and uriHibernate propertiesSelects Ehcache and its ehcache.xml
<cache alias=”…”>ehcache.xmlOne cache per region, with a size limit
@Cacheable and @CacheEntity classes and collection fieldsDecide what goes into the cache

2. Ehcache 3 Setup Example

The following example caches airport lookup data, which every flight search reads and which changes rarely. An Airport has an IATA code (“DEL”), a name (“Indira Gandhi”) and a city (“Delhi”), and an Airline (“Air India”) has a set of hub airports. The project runs on Java 25, Hibernate 7.4.11.Final, Ehcache 3.12.0 and an in-memory H2 database. The complete project on GitHub prints the SQL and the cache statistics of every step, and its 22 JUnit tests check them (mvn -q compile exec:java, mvn test).

2.1. Adding the Maven Dependencies

We need two dependencies next to hibernate-core. Both versions are the latest on the day of writing, and hibernate-jcache always takes the same version as hibernate-core.

<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-jcache</artifactId>
  <version>7.4.11.Final</version>
</dependency>
<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>3.12.0</version>
  <classifier>jakarta</classifier>
  <exclusions>
    <exclusion>
      <groupId>org.glassfish.jaxb</groupId>
      <artifactId>jaxb-runtime</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Ehcache publishes two jars of the same version, and they differ only in the XML library that parses ehcache.xml.

JarParses ehcache.xml withUse it with
ehcache-3.12.0.jarjavax.xml.bind (JAXB 2)Hibernate 5, Java EE
ehcache-3.12.0-jakarta.jar (classifier jakarta)jakarta.xml.bind (JAXB 4)Hibernate 6 and 7, Jakarta EE 9+

With Hibernate 6 or 7, we always add the jakarta classifier. We need the exclusion too, because the Ehcache POM asks for a JAXB 2 runtime with the version range [2.2,3), while hibernate-core already brings jaxb-runtime 4.0.7. Without the exclusion, Maven 3.9 tries to resolve that range and fails before compiling.

[ERROR] Failed to execute goal on project hibernate-ehcache3: Could not collect dependencies for project com.howtodoinjava:hibernate-ehcache3:jar:1.0.0
[ERROR] Failed to read artifact descriptor for javax.xml.bind:jaxb-api:jar:2.3.0-b161121.1438

The javax.cache:cache-api jar comes with hibernate-jcache, so we do not declare it.

2.2. Turning On the Second Level Cache

The Hibernate properties pick the region factory, the provider and the configuration file. We bootstrap with HibernatePersistenceConfiguration, but the keys are the same in persistence.xml or hibernate.properties.

new HibernatePersistenceConfiguration("ehcache3")
    .managedClasses(Airport.class, Airline.class)
    .property("hibernate.cache.use_second_level_cache", true)
    .property("hibernate.cache.region.factory_class", "jcache")
    .property("hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider")
    .property("hibernate.javax.cache.uri", "ehcache.xml")
    .property("hibernate.javax.cache.missing_cache_strategy", "fail")
    .property("hibernate.cache.use_query_cache", true)
    .property("hibernate.generate_statistics", true)
    ...
    .createEntityManagerFactory();

Three of the keys contain javax because they name the JCache package javax.cache, which never moved to jakarta, so the keys stay the same in Hibernate 7.

PropertyValuePurpose
hibernate.cache.use_second_level_cachetrueSwitch the cache on or off; defaults to true once a region factory is set
hibernate.cache.region.factory_classjcacheShort name of JCacheRegionFactory from hibernate-jcache
hibernate.javax.cache.providerorg.ehcache.jsr107.EhcacheCachingProviderThe JCache provider to load
hibernate.javax.cache.uriehcache.xmlThe Ehcache file, read from the classpath (src/main/resources)
hibernate.javax.cache.missing_cache_strategyfailStop at startup when a region has no cache in ehcache.xml
hibernate.cache.use_query_cachetrueAllow cached query results (section 2.6)
hibernate.generate_statisticstrueCount hits, misses and puts (section 2.5)

The default for missing_cache_strategy is create-warn, which means Hibernate logs a warning and creates the missing cache without a size limit. We set it to fail, so a forgotten region stops the application at startup instead of growing without limit in production.

2.3. A Minimal ehcache.xml

The file ehcache.xml declares one <cache> per Hibernate region, and the alias must match the region name character for character. Hibernate builds the region names from our classes.

  • An entity region is named after the entity class, such as com.howtodoinjava.hibernate.ehcache.Airport.
  • A collection region is the class name plus the field name, such as com.howtodoinjava.hibernate.ehcache.Airline.hubs.
  • The query cache uses default-query-results-region and default-update-timestamps-region.

Every region gets a heap size in entries and a time to live (ttl), and a template keeps the shared settings in one place.

<config xmlns="http://www.ehcache.org/v3">

  <cache-template name="lookup">
    <expiry>
      <ttl unit="hours">6</ttl>
    </expiry>
    <heap unit="entries">500</heap>
  </cache-template>

  <cache alias="com.howtodoinjava.hibernate.ehcache.Airport" uses-template="lookup"/>
  <cache alias="com.howtodoinjava.hibernate.ehcache.Airline" uses-template="lookup"/>
  <cache alias="com.howtodoinjava.hibernate.ehcache.Airline.hubs" uses-template="lookup"/>

  <cache alias="default-query-results-region" uses-template="lookup"/>
  <cache alias="default-update-timestamps-region">
    <expiry>
      <none/>
    </expiry>
    <heap unit="entries">500</heap>
  </cache>
</config>

We leave out <key-type> and <value-type>, which some older examples declare, because Hibernate stores its own key objects, not our ids, so a java.lang.Long key type fails on the first put. The Ehcache configuration for Hibernate has more options, such as tti expiry and offheap sizing.

2.4. Marking Entities and Collections as Cacheable

By default, Hibernate caches no entity, so we mark each cached entity with two annotations.

  • @Cacheable (Jakarta Persistence) marks the entity for the shared cache.
  • @Cache (Hibernate) sets the concurrency strategy. READ_WRITE keeps the cache correct when our code updates an airport; READ_ONLY fits data that never changes.
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Airport {

  @Id
  private String iataCode;

  private String name;
  private String city;
}

A cached entity does not cache its collections. The hubs field gets its own @Cache, and Airport, the element type of the collection, is cached too, because the collection entry holds only the airport ids.

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Airline {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

  @ManyToMany
  @JoinTable(name = "airline_hub",
      joinColumns = @JoinColumn(name = "airline_id"),
      inverseJoinColumns = @JoinColumn(name = "airport_code"))
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  private Set<Airport> hubs = new HashSet<>();
}

The hubs field is a @ManyToMany because one airport is the hub of several airlines. The cache holds the column values of an airport, not the Airport object, and each concurrency strategy handles updates in its own way.

2.5. Checking the Cache Hits With Statistics

The SQL log shows whether the cache works, because a cached lookup sends no SELECT. The SessionFactory also counts every cache access when hibernate.generate_statistics is true. We empty the cache and load Delhi airport in two sessions.

Statistics stats = emf.unwrap(SessionFactory.class).getStatistics();
emf.getCache().evictAll();
stats.clear();

emf.runInTransaction(em -> em.find(Airport.class, "DEL"));
// select a1_0.iataCode,a1_0.city,a1_0.name from Airport a1_0 where a1_0.iataCode=?
// L2 hits=0, misses=1, puts=1, statements=1

emf.runInTransaction(em -> em.find(Airport.class, "DEL"));
// no SQL
// L2 hits=1, misses=1, puts=1, statements=1

We check getPrepareStatementCount() together with the hit count, because it proves that no SQL ran. Five counters show whether the cache works.

MethodCounts
getSecondLevelCacheHitCount()Lookups served from Ehcache
getSecondLevelCacheMissCount()Lookups not found in Ehcache
getSecondLevelCachePutCount()Entries written to Ehcache
getPrepareStatementCount()SQL statements sent to the database
getCollectionStatistics(role).getCacheHitCount()Hits for one collection, for example …Airline.hubs

Persisting also fills the cache. Saving three airports and Air India gave puts=4 without any read, so the first session after the insert already gets a hit.

The collection works the same way. The first session runs two statements, one for the airline and one for its hubs, whereas the second session reads all four entries from Ehcache, namely the airline, the hubs collection, Delhi and Mumbai.

emf.runInTransaction(em -> em.find(Airline.class, airlineId).hubCities());
// select a1_0.id,a1_0.name from Airline a1_0 where a1_0.id=?
// select h1_0.airline_id,h1_1.iataCode,h1_1.city,h1_1.name from airline_hub h1_0 join Airport h1_1 on ... where h1_0.airline_id=?
// [Delhi, Mumbai]   L2 hits=0, misses=2, puts=3, statements=2

emf.runInTransaction(em -> em.find(Airline.class, airlineId).hubCities());
// no SQL
// [Delhi, Mumbai]   L2 hits=4, misses=2, puts=3, statements=2

The first session had three puts, not four, because Delhi was already in the cache from the previous lookup.

2.6. Caching Query Results

The entity cache helps only for lookups by id, such as em.find(), whereas a JPQL query runs its SQL every time unless we also cache its result. To cache query results, we set hibernate.cache.use_query_cache=true and opt in each query with a hint.

List<Airport> mumbai = em.createQuery("select a from Airport a where a.city = :city", Airport.class)
    .setParameter("city", "Mumbai")
    .setHint(HibernateHints.HINT_CACHEABLE, true)
    .getResultList();
// session 1: select a1_0.iataCode,a1_0.city,a1_0.name from Airport a1_0 where a1_0.city=?
//            [BOM (Mumbai)], query cache misses=1, puts=1
// session 2: no SQL, [BOM (Mumbai)], query cache hits=1

The same query without the hint ran two SELECT statements in two sessions. Any change to a table drops the cached query results for that table, so after we renamed Delhi airport, the cached Mumbai query ran its SELECT again, although Delhi is not in its result. The query cache is useful only for tables that rarely change, such as airports.

3. Errors From a Missing or Wrong Setting

A wrong setup fails when the EntityManagerFactory starts, and the root cause at the bottom of the stack trace names the missing piece. To see each error, we broke each piece of the checklist on purpose.

Setup checklist with six steps. 1: hibernate-jcache in pom.xml, if missing Unable to resolve name [jcache] as strategy. 2: ehcache with classifier jakarta, if missing NoClassDefFoundError javax/xml/bind/ValidationEventHandler. 3: region.factory_class jcache, if wrong Unable to resolve name [ehcache]. 4: javax.cache.provider and javax.cache.uri, if wrong Failed to load the CachingProvider or Couldn't load URI from ehcache-missing.xml. 5: one cache alias per region in ehcache.xml, if missing On-the-fly creation of JCache Cache objects is not supported. 6: @Cacheable and @Cache on entities and collections, if missing no error but every lookup runs SQL; check with Statistics
Each missing piece has its own startup error. Only the annotations fail quietly, which is why we check the statistics.
What is wrongRoot cause in the stack trace
hibernate-jcache missingClassNotFoundException: Could not load requested class: jcache
ehcache without classifier jakartaNoClassDefFoundError: javax/xml/bind/ValidationEventHandler
region.factory_class=ehcache (the Hibernate 5 name)ClassNotFoundException: Could not load requested class: ehcache
Wrong provider class nameClassNotFoundException: org.ehcache.EhcacheProvider
ehcache.xml not on the classpathCacheException: Couldn’t load URI from ehcache-missing.xml
A region missing from ehcache.xml, strategy failCacheException: On-the-fly creation of JCache Cache objects is not supported […Airline.hubs]
No hibernate.javax.cache.uri, strategy failCacheException: On-the-fly creation of JCache Cache objects is not supported [default-update-timestamps-region]

Without hibernate-jcache, Hibernate does not know the short name jcache.

org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.cache.spi.RegionFactory] due to: Unable to resolve name [jcache] as strategy [org.hibernate.cache.spi.RegionFactory]
Caused by: org.hibernate.boot.registry.selector.spi.StrategySelectionException: Unable to resolve name [jcache] as strategy [org.hibernate.cache.spi.RegionFactory]
Caused by: org.hibernate.boot.registry.classloading.spi.ClassLoadingException: Unable to load class [jcache]
Caused by: java.lang.ClassNotFoundException: Could not load requested class: jcache

The plain ehcache jar starts to read ehcache.xml with JAXB 2 classes that are not on a Hibernate 7 classpath.

java.lang.NoClassDefFoundError: javax/xml/bind/ValidationEventHandler
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.ValidationEventHandler

A wrong file name in hibernate.javax.cache.uri stops the cache provider.

jakarta.persistence.PersistenceException: Unable to build Hibernate SessionFactory  [persistence unit: ehcache3]
Caused by: org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.cache.spi.CacheImplementor] due to: Cache provider not started
Caused by: java.lang.IllegalStateException: Cache provider not started
Caused by: org.hibernate.cache.CacheException: Couldn't load URI from ehcache-missing.xml

A region without a <cache> element fails only with the fail strategy. We removed the Airline.hubs cache from the file.

jakarta.persistence.PersistenceException: Unable to build Hibernate SessionFactory  [persistence unit: ehcache3]
Caused by: org.hibernate.cache.CacheException: On-the-fly creation of JCache Cache objects is not supported [com.howtodoinjava.hibernate.ehcache.Airline.hubs]

With the default create-warn, the same file starts with a warning, and Hibernate creates the hubs cache without a size limit.

WARN org.hibernate.orm.cache - HHH90001006: Missing cache region [com.howtodoinjava.hibernate.ehcache.Airline.hubs] was created with provider-specific default policies. Explicitly configure the region and its policies, or disable this warning by setting 'hibernate.javax.cache.missing_cache_strategy' to 'create'.

Forgotten annotations do not cause any error. Without @Cacheable or @Cache, the entity is not cached and every session runs the SELECT. The statistics from section 2.5 are the only way to notice it.

4. Ehcache 3 With Hibernate FAQs

4.1. How Do I Configure Ehcache 3 in Spring Boot 4?

Spring Boot has no dedicated property for the Hibernate second level cache, so we pass the same Hibernate keys with the spring.jpa.properties. prefix, which Spring Boot passes to Hibernate unchanged.

spring.jpa.properties.hibernate.cache.region.factory_class=jcache
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.jpa.properties.hibernate.javax.cache.uri=ehcache.xml
spring.jpa.properties.hibernate.javax.cache.missing_cache_strategy=fail
spring.jpa.properties.hibernate.generate_statistics=true

A few details differ from a plain Hibernate project.

  • Spring Boot 4.1.1 manages the versions of hibernate-jcache and of ehcache with the jakarta classifier, so the dependencies need no <version>. It also manages jaxb-runtime (4.0.9), which replaces the JAXB 2 range, so the exclusion from section 2.1 is not needed.
  • The properties spring.cache.jcache.config and spring.cache.jcache.provider configure Spring’s own method cache (@EnableCaching with Spring’s @Cacheable). They do not reach Hibernate.
  • Some tutorials write hibernate.cache.jcache.uri, but Hibernate does not know that key and ignores it, so Ehcache starts without our file. In our test, it failed with the same On-the-fly creation … [default-update-timestamps-region] error as a missing URI.

4.2. Does This Setup Work With Hibernate 6?

Yes. We ran the same entities, ehcache.xml and properties on Hibernate 6.6.58.Final with Java 25. Delhi airport was loaded with one SELECT and read from Ehcache in the second session (hits=1, misses=1, statements=1). The differences are outside the cache setup.

Hibernate 6.6Hibernate 7.4
hibernate-jcache versionSame as hibernate-core, for example 6.6.58.Final7.4.11.Final
ehcache dependency3.12.0, classifier jakarta, same exclusionSame
Cache propertiesSame keysSame keys
Bootstrap in the exampleConfiguration and SessionFactory.inTransaction()HibernatePersistenceConfiguration and emf.runInTransaction()

4.3. Which Old Dependencies Can I Remove?

Hibernate 6 and 7 no longer need the jars that older setups list, and some of them break the build.

Old dependencyStatus today
org.hibernate:hibernate-ehcacheFor Ehcache 2 only; the last final release is 5.6.15.Final. Remove it
net.sf.ehcache:ehcache (Ehcache 2)Replaced by org.ehcache:ehcache 3.x
javax.persistence:javax.persistence-apiHibernate 6 and 7 use jakarta.persistence. Remove it
javax.xml.bind:jaxb-api, com.sun.xml.bind:jaxb-core, jaxb-implOnly the plain Ehcache jar needs them. The jakarta jar uses the JAXB 4 runtime that comes with hibernate-core
hibernate.cache.region.factory_class=ehcacheUnknown in Hibernate 6+; use jcache

Even older Hibernate 3 configurations used the CacheProvider interface, which no longer exists at all.

4.4. Do I Have to Set hibernate.cache.region.factory_class?

Not when hibernate-jcache is the only cache module on the classpath. We removed both region.factory_class and use_second_level_cache, and the second session still read Delhi from the cache (hits=1, statements=1). Hibernate finds the single region factory that the module registers. We still set the property, because it fails fast with a clear message when the module is missing, and it keeps the configuration readable.

5. Conclusion

Ehcache 3 runs with Hibernate 6 and 7 through JCache. We add hibernate-jcache and the ehcache jar with the jakarta classifier, and set the jcache region factory with the Ehcache provider and an ehcache.xml. In that file, we declare one cache per region, and we mark entities and collections with @Cacheable and @Cache.

The setting missing_cache_strategy=fail catches forgotten regions at startup, and the statistics prove that the second session reads from the cache instead of the database.

6. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Just noticed that hibernate-test.cfg.xml has this on line 30. When set to true, the tests pass. Thanks again!

        <property name=”hibernate.generate_statistics”>false</property>

  2. Thanks for the article and working code. Know why the 2nd assertion in TestSecondLevelCache.java fails with:

    Expected :1

    Actual  :0

    com.howtodoinjava.basics.TestSecondLevelCache.testL2Cahce(TestSecondLevelCache.java:92)

  3. Just one big confusion. When hibernate-jcache library is there for integration and API support then why we need hibernate-ehcache jar? Is it deprecated? if hibernate-ehcache is deprecated then can i only use hiernate-jcache lib to communicate with ehcache 3 lib?

    Thanks for your time.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.