I'm trying to configure EHCache with Hibernate in Spring Boot 2.2, but it seems I'm doing something wrong. I've looked at several tutorials and SO questions but didn't find something that matched fully my approach.
I chose the approach of a no-XML, jcache configuration for the caching.
However, Hibernate doesn't detect the existing cache manager (I checked and even enforced with @AutoconfigureBefore
: the cache manager is loaded before the Hibernate auto-configuration).
As a result, Hibernate creates a second EhcacheManager
and throws several warnings such as the following:
HHH90001006: Missing cache[com.example.demo.one.dto.MyModel] was created on-the-fly. The created cache will use a provider-specific default configuration: make sure you defined one. You can disable this warning by setting 'hibernate.javax.cache.missing_cache_strategy' to 'create'.
I tried to use a HibernatePropertiesCustomizer
to tell Hibernate which cache manager it should use.
The bean is instanciated, but never called, so it loses all its appeal and purpose.
Does somebody know what I'm doing wrong and how I should intimate Hibernate to use the cache manager I already configured rather than creating his own?
I compared my configuration with the one JHipster generates.
It looks very similar, though their HibernatePropertiesCustomizer
is called.
I did not succeed in identifying the difference between their cache configuration and mine.
This appears tobe related to my data source configuration (see code below).
I tried removing it and enabling my JPA configuration in a simpler way, and the HibernatePropertiesCustomizer
is indeed called as expected.
@SpringBootApplication
@EnableTransactionManagement
@EnableJpaRepositories("com.example.demo.one.repository")
public class DemoApplication {
Actually, having configured my datasources manually (because I need to handle two distinct datasources), I circumvent Spring Boot's DataSourceAutoConfiguration
, and its HibernateJpaAutoConfiguration
is not applied.
This autoconfiguration is the one that applies the HibernatePropertiesCustomizer
(rather, it calls HibernateJpaConfiguration
to do it).
However, I'm not sure how I should call this configuration to apply it.
I use the following dependencies (I let spring-boot-starter-parent
set the versions):
package com.example.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.cache.CacheManager;
import java.time.Duration;
@Configuration
@EnableCaching
@Slf4j
//@AutoConfigureBefore(value = {DataSource1Config.class, DataSource2Config.class})
public class CacheConfiguration {
private static final int TIME_TO_LIVE_SECONDS = 240;
private static final int MAX_ELEMENTS_DEFAULT = 200;
// Create this configuration as a bean so that it is used to customize automatically created caches
@Bean
public javax.cache.configuration.Configuration<Object, Object> jcacheConfiguration() {
final org.ehcache.config.CacheConfiguration<Object, Object> cacheConfiguration =
CacheConfigurationBuilder
.newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(MAX_ELEMENTS_DEFAULT))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(TIME_TO_LIVE_SECONDS)))
.build();
return Eh107Configuration.fromEhcacheCacheConfiguration(
cacheConfiguration
);
}
@Bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) {
log.error(">>>>>>>>>>>> customizer setup"); // Printed
return hibernateProperties -> {
log.error(">>>>>>>>>>>> customizer called"); // Not printed
hibernateProperties.put("hibernate.javax.cache.cache_manager", cacheManager);
};
}
@Bean
public JCacheManagerCustomizer cacheManagerCustomizer(javax.cache.configuration.Configuration<Object, Object> jcacheConfiguration) {
return cm -> {
createCache(cm, com.example.demo.one.dto.MyModel.class.getName(), jcacheConfiguration);
};
}
private void createCache(CacheManager cm, String cacheName, javax.cache.configuration.Configuration<Object, Object> jcacheConfiguration) {
javax.cache.Cache<Object, Object> cache = cm.getCache(cacheName);
if (cache != null) {
cm.destroyCache(cacheName);
}
cm.createCache(cacheName, jcacheConfiguration);
}
}
I have two data sources.
The second one is similar to this one, minus the @Primary
annotations.
Removing the second datasource does not solve the issue.
package com.example.demo.config;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.demo.one.repository",
entityManagerFactoryRef = "dataSource1EntityManagerFactory",
transactionManagerRef = "transactionManager1"
)
public class DataSource1Config {
@Bean
@Primary
@ConfigurationProperties(prefix = "datasource.one")
public DataSourceProperties dataSource1Properties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSource1(DataSourceProperties dataSource1Properties) {
return dataSource1Properties.initializeDataSourceBuilder().build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean dataSource1EntityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dataSource1) {
return builder
.dataSource(dataSource1)
.packages("com.example.demo.one.dto")
.build();
}
@Bean
@Primary
public PlatformTransactionManager transactionManager1(EntityManagerFactory dataSource1EntityManagerFactory) {
return new JpaTransactionManager(dataSource1EntityManagerFactory);
}
}
spring:
jpa:
database: <my-db>
hibernate:
ddl-auto: validate
properties:
hibernate:
dialect: <my-dialect>
jdbc.time_zone: UTC
javax:
cache:
#missing_cache_strategy: fail # Useful for testing if Hibernate creates a second cache manager
cache:
use_second_level_cache: true
use_query_cache: false
region.factory_class: jcache
It's not been easy but I found the cause and solution.
Basically, the problem comes from the fact that I configure the LocalContainerEntityManagerFactoryBean
myself.
If you don't, Spring Boot will use its AutoConfigurations to create all nice and well, including vendor properties (everything you have under spring.jpa.properties
), hibernate properties (everything under spring.jpa.hibernate
), and applying defaults and customizations, among which my long-looked for HibernateJpaAutoConfiguration
.
But since I needed to have several datasources, I bypassed all that and, listening to my tutorials, I did the lazy following.
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean dataSource1EntityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dataSource1) {
return builder
.dataSource(dataSource1)
.packages("com.example.demo.one.dto")
.build();
}
The solution is almost simple: do everything Spring Boot would do. "Almost" only, because most of those mechanisms rely on AutoConfigurations (overriding those is a code smell, so that's not the way to do it) and/or internal/protected classes (which you cannot call directly).
This means you essentially have to copy Spring Boot's code into your own, possibly creating some brittleness regarding future upgrades of Spring Boot (or simply that your code won't benefit from the latest bug/performances fixes). From that regard, I'm not a huge fan of the solution I present here.
You will need to inject the following beans into your data source configuration :
org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties
org.springframework.boot.autoconfigure.orm.jpa.JpaProperties
List<org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer>
Drawing on org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
, I added a hibernate.resource.beans.container
property customizer.
However, I skipped the naming policies which are not an issue in our project.
This gives me the following constructor and method:
public DataSource1Config(
JpaProperties jpaProperties,
HibernateProperties hibernateProperties,
ConfigurableListableBeanFactory beanFactory,
ObjectProvider<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers
) {
this.jpaProperties = jpaProperties;
this.hibernateProperties = hibernateProperties;
this.hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
beanFactory,
hibernatePropertiesCustomizers.orderedStream().collect(Collectors.toList())
);
}
private List<HibernatePropertiesCustomizer> determineHibernatePropertiesCustomizers(
ConfigurableListableBeanFactory beanFactory,
List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers
) {
List<HibernatePropertiesCustomizer> customizers = new ArrayList<>();
if (ClassUtils.isPresent("org.hibernate.resource.beans.container.spi.BeanContainer",
getClass().getClassLoader())) {
customizers.add((properties) -> properties.put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory)));
}
customizers.addAll(hibernatePropertiesCustomizers);
return customizers;
}
Then, drawing on org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration
, I loaded the vendor properties.
Here again, I skipped some automatic customizations that you may to have a look upon (JpaBaseConfiguration#customizeVendorProperties(Map)
and its implementation in subclasses).
private Map<String, Object> getVendorProperties() {
return new LinkedHashMap<>(
this.hibernateProperties
.determineHibernateProperties(jpaProperties.getProperties(),
new HibernateSettings()
// Spring Boot's HibernateDefaultDdlAutoProvider is not available here
.hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers)
)
);
}
Just as a reference, I give you my complete configuration class once I applied the changes detailed above.
package com.example.demo.config;
import org.hibernate.cfg.AvailableSettings;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.hibernate5.SpringBeanContainer;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.util.ClassUtils;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.demo.one.repository",
entityManagerFactoryRef = "dataSource1EntityManagerFactory",
transactionManagerRef = "TransactionManager1"
)
public class DataSource1Config {
private final JpaProperties jpaProperties;
private final HibernateProperties hibernateProperties;
private final List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers;
public DataSource1Config(
JpaProperties jpaProperties,
HibernateProperties hibernateProperties,
ConfigurableListableBeanFactory beanFactory,
ObjectProvider<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers
) {
this.jpaProperties = jpaProperties;
this.hibernateProperties = hibernateProperties;
this.hibernatePropertiesCustomizers = determineHibernatePropertiesCustomizers(
beanFactory,
hibernatePropertiesCustomizers.orderedStream().collect(Collectors.toList())
);
}
private List<HibernatePropertiesCustomizer> determineHibernatePropertiesCustomizers(
ConfigurableListableBeanFactory beanFactory,
List<HibernatePropertiesCustomizer> hibernatePropertiesCustomizers
) {
List<HibernatePropertiesCustomizer> customizers = new ArrayList<>();
if (ClassUtils.isPresent("org.hibernate.resource.beans.container.spi.BeanContainer",
getClass().getClassLoader())) {
customizers.add((properties) -> properties.put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory)));
}
customizers.addAll(hibernatePropertiesCustomizers);
return customizers;
}
@Bean
@Primary
@ConfigurationProperties(prefix = "datasource.lib")
public DataSourceProperties dataSource1Properties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSource1(DataSourceProperties dataSource1Properties) {
return dataSource1Properties.initializeDataSourceBuilder().build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean dataSource1EntityManagerFactory(EntityManagerFactoryBuilder factoryBuilder, DataSource dataSource1) {
final Map<String, Object> vendorProperties = getVendorProperties();
return factoryBuilder
.dataSource(dataSource1)
.packages("com.example.demo.one.dto")
.properties(vendorProperties)
.build();
}
@Bean
@Primary
public PlatformTransactionManager transactionManager1(EntityManagerFactory dataSource1EntityManagerFactory) {
return new JpaTransactionManager(dataSource1EntityManagerFactory);
}
private Map<String, Object> getVendorProperties() {
return new LinkedHashMap<>(
this.hibernateProperties
.determineHibernateProperties(jpaProperties.getProperties(),
new HibernateSettings()
// Spring Boot's HibernateDefaultDdlAutoProvider is not available here
.hibernatePropertiesCustomizers(this.hibernatePropertiesCustomizers)
)
);
}
}