Как использовать Spring управляемые перехватчики Hibernate в Spring Boot?
Можно ли интегрировать управляемые перехватчики Hibernate Spring (http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html) в Spring Загрузка?
Я использую Spring Data JPA и Spring Data REST и нуждаюсь в перехватчике Hibernate, чтобы воздействовать на обновление определенного поля на сущности.
При стандартных событиях JPA невозможно получить старые значения, и, следовательно, я думаю, что мне нужно использовать перехватчик Hibernate.
Ответы
Ответ 1
Нет особенно простого способа добавить перехватчик Hibernate, который также является Spring Bean, но вы можете легко добавить перехватчик, если он полностью управляется Hibernate. Для этого добавьте в свой application.properties
следующее:
spring.jpa.properties.hibernate.ejb.interceptor=my.package.MyInterceptorClassName
Если вам нужно, чтобы Interceptor также был бобом, вы можете создать свой собственный LocalContainerEntityManagerFactoryBean
. EntityManagerFactoryBuilder
из Spring Boot 1.1.4 немного ограничен родовым свойством свойств, поэтому вам нужно привести к (Map)
, мы посмотрим, как это исправить для 1.2.
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder factory, DataSource dataSource,
JpaProperties properties) {
Map<String, Object> jpaProperties = new HashMap<String, Object>();
jpaProperties.putAll(properties.getHibernateProperties(dataSource));
jpaProperties.put("hibernate.ejb.interceptor", hibernateInterceptor());
return factory.dataSource(dataSource).packages("sample.data.jpa")
.properties((Map) jpaProperties).build();
}
@Bean
public EmptyInterceptor hibernateInterceptor() {
return new EmptyInterceptor() {
@Override
public boolean onLoad(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
System.out.println("Loaded " + id);
return false;
}
};
}
Ответ 2
Взяв за основу несколько потоков, я получил следующее решение:
Я использую Spring-Boot 1.2.3.RELEASE (текущее значение ga на данный момент)
Мой пример использования описан в этой ошибке (DATAREST-373).
Мне нужно было иметь возможность кодировать пароль User
@Entity
при создании и иметь специальную логику при сохранении. Создание было очень простым с использованием @HandleBeforeCreate
и проверкой идентификатора @Entity
на равенство 0L
.
Для сохранения я реализовал Hibernate Interceptor, который расширяет EmptyInterceptor
@Component
class UserInterceptor extends EmptyInterceptor{
@Autowired
PasswordEncoder passwordEncoder;
@Override
boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
if(!(entity instanceof User)){
return false;
}
def passwordIndex = propertyNames.findIndexOf { it == "password"};
if(entity.password == null && previousState[passwordIndex] !=null){
currentState[passwordIndex] = previousState[passwordIndex];
}else{
currentState[passwordIndex] = passwordEncoder.encode(currentState[passwordIndex]);
}
return true;
}
}
При использовании весенней загрузки в документации говорится, что
все свойства в spring.jpa.properties. * передаются как обычные свойства JPA (с удаленным префиксом) при создании локального EntityManagerFactory.
Как указано во многих ссылках, мы можем определить наш перехватчик, используя spring.jpa.properties.hibernate.ejb.interceptor
в нашей конфигурации Spring-Boot. Однако я не мог заставить @Autowire PasswordEncoder
работать.
Поэтому я прибег к использованию HibernateJpaAutoConfiguration и переопределению protected void customizeVendorProperties(Map<String, Object> vendorProperties)
. Вот моя конфигурация.
@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{
@Autowired
Interceptor userInterceptor;
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
vendorProperties.put("hibernate.ejb.interceptor",userInterceptor);
}
}
Автоматическое подключение Interceptor
вместо того, чтобы позволить Hibernate создавать его экземпляры, было ключом к тому, чтобы заставить его работать.
Что меня сейчас беспокоит, так это то, что логика разделена на две части, но, надеюсь, после того, как DATAREST-373 будет решен, это не понадобится.
Ответ 3
Мой простой пример файла спящего режима для spring загрузки (spring -boot-starter 1.2.4.RELEASE)
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.*;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jpa.HibernateEntityManagerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.persistence.EntityManagerFactory;
@Component
public class UiDateListener implements PostLoadEventListener, PreUpdateEventListener {
@Inject EntityManagerFactory entityManagerFactory;
@PostConstruct
private void init() {
HibernateEntityManagerFactory hibernateEntityManagerFactory = (HibernateEntityManagerFactory) this.entityManagerFactory;
SessionFactoryImpl sessionFactoryImpl = (SessionFactoryImpl) hibernateEntityManagerFactory.getSessionFactory();
EventListenerRegistry registry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);
registry.appendListeners(EventType.POST_LOAD, this);
registry.appendListeners(EventType.PRE_UPDATE, this);
}
@Override
public void onPostLoad(PostLoadEvent event) {
final Object entity = event.getEntity();
if (entity == null) return;
// some logic after entity loaded
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
final Object entity = event.getEntity();
if (entity == null) return false;
// some logic before entity persist
return false;
}
}
Ответ 4
У меня была аналогичная проблема с приложением Spring 4.1.1, Hibernate 4.3.11 - not Spring Boot.
Решение, которое я нашел (после чтения кода Hibernate EntityManagerFactoryBuilderImpl), состоял в том, что если вы передадите ссылку bean вместо имени класса в hibernate.ejb.interceptor
свойство определения менеджера сущностей, Hibernate будет использовать уже созданный экземпляр bean.
Итак, в моем определении entityManager в контексте приложения у меня было что-то вроде этого:
<bean id="auditInterceptor" class="com.something.AuditInterceptor" />
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
...>
<property name="jpaProperties">
<map>
...
<entry key="hibernate.ejb.interceptor">
<ref bean="auditInterceptor" />
</entry>
...
</map>
</property>
</bean>
АудитInterceptor управляется Spring, поэтому для него будет доступно autowiring и другие Spring -натуральные поведения.
Ответ 5
Здравствуйте,
Прочитайте это: https://github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be (используйте: интерфейс HibernatePropertiesCustomizer)
ИЛИ
Для простого перехватчика:
Чтобы настроить это в своем приложении, вам просто необходимо добавить: spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor (в application.properties). Сам перехватчик должен быть @Component.
Пока перехватчик фактически не использует никаких бобов. В противном случае это немного сложнее, но я был бы более чем рад предложить решение.
Не забудьте добавить в application-test.properties EmptyInterceptor, чтобы не использовать систему ведения журналов (или то, для чего вы хотите ее использовать) в тестах (что не очень полезно).
Надеюсь, что это было полезно для вас.
И последнее замечание: всегда обновляйте свои версии Spring/Hibernate (используйте самые последние версии), и вы увидите, что большая часть кода станет избыточной, так как более новые версии стараются максимально уменьшить конфигурации.
Ответ 6
Я нашел другой подход после исследования двух дней о том, как интегрировать Hibernate Interceptors с Spring Data JPA, мое решение является гибридом между конфигурацией java и конфигурацией xml, но это сообщение было очень полезно. Итак, моим окончательным решением было:
Класс AuditLogInterceptor:
public class AuditLogInterceptor extends EmptyInterceptor{
private int updates;
//interceptor for updates
public boolean onFlushDirty(Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
updates++;
for ( int i=0; i < propertyNames.length; i++ ) {
if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
currentState[i] = new Date();
return true;
}
}
}
return false;
}
}
Конфигурация Java для источника данных:
@Bean
DataSource dataSource() {
//Use JDBC Datasource
DataSource dataSource = new DriverManagerDataSource();
((DriverManagerDataSource)dataSource).setDriverClassName(jdbcDriver);
((DriverManagerDataSource)dataSource).setUrl(jdbcUrl);
((DriverManagerDataSource)dataSource).setUsername(jdbcUsername);
((DriverManagerDataSource)dataSource).setPassword(jdbcPassword);
return dataSource;
}
Менеджеры сущностей и транзакций, добавляющие Interceptor
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:persistenceUnitName="InterceptorPersistentUnit" p:persistenceXmlLocation="classpath:audit/persistence.xml"
p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaAdapter">
<property name="loadTimeWeaver">
<bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
p:entityManagerFactory-ref="entityManagerFactory" />
<bean id="jpaAdapter"
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
p:database="ORACLE" p:showSql="true" />
конфигурационный файл сохранения
<persistence-unit name="InterceptorPersistentUnit">
<class>com.app.CLASSTOINTERCEPT</class>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
<properties>
<property name="hibernate.ejb.interceptor"
value="com.app.audit.AuditLogInterceptor" />
</properties>
</persistence-unit>
Ответ 7
Я столкнулся с той же проблемой и завел небольшую библиотеку пружин, чтобы справиться со всеми настройками.
https://github.com/teastman/spring-data-hibernate-event
Если вы используете Spring Boot, просто добавьте зависимость:
<dependency>
<groupId>io.github.teastman</groupId>
<artifactId>spring-data-hibernate-event</artifactId>
<version>1.0.0</version>
</dependency>
Затем добавьте аннотацию @HibernateEventListener в любой метод, где первый параметр - это объект, который вы хотите прослушивать, а второй параметр - событие Hibernate, которое вы хотите прослушивать. Я также добавил статическую функцию util getPropertyIndex, чтобы упростить получение доступа к определенному свойству, которое вы хотите проверить, но вы также можете просто посмотреть на необработанное событие Hibernate.
@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
int index = getPropertyIndex(event, "name");
if (event.getOldState()[index] != event.getState()[index]) {
// The name changed.
}
}
Ответ 8
Решение с использованием Spring Boot 2
@Component
public class MyInterceptorRegistration implements HibernatePropertiesCustomizer {
@Autowired
private MyInterceptor myInterceptor;
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("hibernate.session_factory.interceptor", myInterceptor);
}
}
- Я использую Spring Boot 2.1.7.RELEASE.
- Вместо
hibernate.session_factory.interceptor
вы можете использовать hibernate.ejb.interceptor
. Оба свойства работают, вероятно, из-за требования обратной совместимости.
Почему HibernatePropertiesCustomizer вместо application.properties
Один из предложенных ответов - указать ваш перехватчик в свойстве spring.jpa.properties.hibernate.ejb.interceptor
в application.properties/yml. Эта идея может не сработать, если ваш перехватчик находится в lib, который будет использоваться несколькими приложениями. Вы хотите, чтобы ваш перехватчик был активирован, просто добавив зависимость в вашу библиотеку, не требуя, чтобы каждое приложение изменило свои application.properties.
Ответ 9
Поскольку перехватчик не регистрируется как подпружиненный компонент, вы можете использовать утилиту, которая может получить экземпляр ApplicationContext
, например:
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtil.applicationContext=applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
Затем вы можете позвонить в службу перехватчика следующим образом:
public class SimpleInterceptor extends EmptyInterceptor {
@Override
public String onPrepareStatement(String sql) {
MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
myService.print();
return super.onPrepareStatement(sql);
}
}