hibernatespring-bootjcacheehcache-3spring-boot-2

Configuring caching for Hibernate with Spring Boot 2.1+


Context and question

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.

Notes from later tests (edits)

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.

Code samples

Dependencies

I use the following dependencies (I let spring-boot-starter-parent set the versions):

Cache config

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);
    }
}

Data source configuration

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);
    }
}

application.yml

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

Solution

  • It's not been easy but I found the cause and solution.

    Cause

    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();
        }
    

    Solution

    In a nutshell

    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).

    Possible brittleness?

    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.

    Detailed guide

    Beans you depend on

    You will need to inject the following beans into your data source configuration :

    Operations to perform

    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)
                    )
            );
        }
    

    Complete configuration class

    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)
                    )
            );
        }
    }