Подтвердить что ты не робот

Как использовать 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.

4b9b3361

Ответ 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);
   }
 }