Настройка кеширования для Hibernate с помощью Spring Boot 2.1+
Контекст и вопрос
Я пытаюсь настроить EHCache с Hibernate в Spring Boot 2.2, но, похоже, что-то делаю не так. Я просмотрел несколько руководств и вопросов SO, но не нашел ничего, что полностью соответствовало бы моему подходу.
Я выбрал для кэширования подход конфигурации jcache без XML. Однако Hibernate не обнаруживает существующий менеджер кеша (я проверил и даже применил@AutoconfigureBefore
: менеджер кеша загружается до автоконфигурации Hibernate). В результате Hibernate создает второйEhcacheManager
и выдает несколько предупреждений, например:
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'.
Я пытался использовать HibernatePropertiesCustomizer
чтобы сообщить Hibernate, какой менеджер кеша он должен использовать. Компонент создается, но никогда не вызывается, поэтому он теряет всю привлекательность и предназначение.
Кто-нибудь знает, что я делаю не так и как мне настроить Hibernate, чтобы использовать уже настроенный мной диспетчер кеша, а не создавать свой собственный?
Я сравнил свою конфигурацию с той, которую генерирует JHipster. Выглядит очень похоже, хотя ихHibernatePropertiesCustomizer
это называется. Мне не удалось определить разницу между их конфигурацией кеша и моей.
Примечания из более поздних тестов (правки)
Похоже, это связано с моей конфигурацией источника данных (см. Код ниже). Я попытался удалить его и включить конфигурацию JPA более простым способом, аHibernatePropertiesCustomizer
действительно называется, как и ожидалось.
@SpringBootApplication
@EnableTransactionManagement
@EnableJpaRepositories("com.example.demo.one.repository")
public class DemoApplication {
Фактически, настроив мои источники данных вручную (потому что мне нужно обрабатывать два разных источника данных), я обхожу Spring Boot DataSourceAutoConfiguration
, и егоHibernateJpaAutoConfiguration
не применяется. Эта автоконфигурация - та, которая применяетHibernatePropertiesCustomizer
(скорее, он вызывает HibernateJpaConfiguration
сделать это). Однако я не уверен, как назвать эту конфигурацию, чтобы применить ее.
Примеры кода
Зависимости
Я использую следующие зависимости (разрешил spring-boot-starter-parent
установить версии):
- org.springframework.boot: весна-загрузка-стартер-данные-JPA
- org.springframework.boot: весна-загрузка-стартер-кеш
- org.hibernate: спящий режим-jcache
- javax.cache: cache-api
- org.ehcache:ehcache
- org.projectlombok: ломбок как утешение
Конфигурация кеша
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);
}
}
Конфигурация источника данных
У меня есть два источника данных. Второй похож на этот, за исключением@Primary
аннотации. Удаление второго источника данных не решает проблему.
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
2 ответа
Это было непросто, но я нашел причину и решение.
Причина
В основном проблема связана с тем, что я настраиваю LocalContainerEntityManagerFactoryBean
себя.
Если вы этого не сделаете, Spring Boot будет использовать свои автоконфигурации для создания всего хорошего и хорошего, включая свойства поставщика (все, что у вас есть в spring.jpa.properties
), свойства гибернации (все, что находится под spring.jpa.hibernate
) и применение значений по умолчанию и настроек, среди которых я давно искал HibernateJpaAutoConfiguration
.
Но так как мне нужно было иметь несколько источников данных, я обошел все это и, слушая свои руководства, я лениво следил за ними.
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean dataSource1EntityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dataSource1) {
return builder
.dataSource(dataSource1)
.packages("com.example.demo.one.dto")
.build();
}
Решение
В двух словах
Решение почти простое: делайте все, что делает Spring Boot. Только "почти", потому что большинство этих механизмов полагаются на автоконфигурации (их переопределение - это запах кода, так что это не способ делать это) и / или внутренние / защищенные классы (которые вы не можете вызвать напрямую).
Возможная хрупкость?
Это означает, что вам, по сути, нужно скопировать код Spring Boot в свой собственный, что может создать некоторую уязвимость в отношении будущих обновлений Spring Boot (или просто то, что ваш код не получит выгоды от последних исправлений ошибок / производительности). В связи с этим я не большой поклонник решения, которое я представляю здесь.
Подробное руководство
Бобы, от которых ты зависишь
Вам нужно будет ввести следующие bean-компоненты в конфигурацию вашего источника данных:
org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties
org.springframework.boot.autoconfigure.orm.jpa.JpaProperties
List<org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer>
Операции для выполнения
Опираясь на org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration
, Я добавил hibernate.resource.beans.container
настройщик свойств. Однако я пропустил политику именования, которая не является проблемой в нашем проекте.
Это дает мне следующий конструктор и метод:
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;
}
Затем, опираясь на org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration
, Я загрузил свойства поставщика. Здесь я снова пропустил некоторые автоматические настройки, на которые вы можете обратить внимание (JpaBaseConfiguration#customizeVendorProperties(Map)
и его реализация в подклассах).
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)
)
);
}
Полный класс конфигурации
В качестве справки я дам вам свой полный класс конфигурации после применения описанных выше изменений.
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)
)
);
}
}
Вы можете просто ввести и поставить cacheManager
в твоем dataSource1EntityManagerFactory
. Не нужно использоватьHibernatePropertiesCustomizer
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean dataSource1EntityManagerFactory(
EntityManagerFactoryBuilder builder,
DataSource dataSource1,
javax.cache.CacheManager cacheManager) {
return builder
.dataSource(dataSource1)
.properties(Map.of(org.hibernate.cache.jcache.ConfigSettings.CACHE_MANAGER, cacheManager))
.packages("com.example.demo.one.dto")
.build();
}