web-dev-qa-db-fra.com

Comment utiliser les intercepteurs Hibernate gérés par Spring dans Spring Boot?

Est-il possible d'intégrer des intercepteurs Hibernate gérés par Spring ( http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html ) dans Spring Boot?

J'utilise Spring Data JPA et Spring Data REST et j'ai besoin d'un intercepteur Hibernate pour agir sur la mise à jour d'un champ particulier sur une entité.

Avec les événements JPA standard, il n'est pas possible d'obtenir les anciennes valeurs. C'est pourquoi je pense que je dois utiliser l'intercepteur Hibernate.

26
Marcel Overdijk

Il n’est pas un moyen particulièrement facile d’ajouter un intercepteur Hibernate qui est aussi un haricot printanier, mais vous pouvez facilement ajouter un intercepteur s’il est entièrement géré par Hibernate. Pour ce faire, ajoutez ce qui suit à votre application.properties:

spring.jpa.properties.hibernate.ejb.interceptor=my.package.MyInterceptorClassName

Si vous avez besoin que l'Interceptor soit aussi un haricot, vous pouvez créer votre propre LocalContainerEntityManagerFactoryBean. La EntityManagerFactoryBuilder de Spring Boot 1.1.4 est un peu trop restrictive avec le générique des propriétés, vous devez donc attribuer cast à (Map), nous allons corriger cela pour la version 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;
        }
    };
}
35
Phil Webb

En prenant les différents threads comme référence, je me suis retrouvé avec la solution suivante:

J'utilise Spring-Boot 1.2.3.RELEASE (qui est le ga actuel)

Mon cas d'utilisation était celui décrit dans ce bogue (DATAREST-373)

J'avais besoin de pouvoir encoder le mot de passe d'une User@Entity sur create, et une logique spéciale sur save. La création était très simple en utilisant @HandleBeforeCreate et en vérifiant l'ID @Entity pour l'égalité 0L

Pour la sauvegarde, j'ai implémenté un Hibernate Interceptor qui étend un 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;

    }
}

En utilisant Spring Boot, la documentation indique que 

toutes les propriétés de spring.jpa.properties. * sont transmises en tant que propriétés JPA normales (avec le préfixe dépouillé) lors de la création de l'EntityManagerFactory local.

Comme de nombreuses références l'ont indiqué, nous pouvons définir notre intercepteur à l'aide de spring.jpa.properties.hibernate.ejb.interceptor dans notre configuration Spring-Boot. Cependant, je n'ai pas pu faire fonctionner le @Autowire PasswordEncoder.

J'ai donc eu recours à HibernateJpaAutoConfiguration et à la substitution de protected void customizeVendorProperties(Map<String, Object> vendorProperties). Voici ma configuration.

@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{


    @Autowired
    Interceptor userInterceptor;


    @Override
    protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
        vendorProperties.put("hibernate.ejb.interceptor",userInterceptor);
    }
}

Autravailler la Interceptor au lieu de permettre à Hibernate de l'instancier était la clé pour la faire fonctionner.

Ce qui me dérange maintenant, c’est que la logique est scindée en deux, mais une fois que DATAREST-373 aura été résolu, cela ne sera plus nécessaire.

15

Mon exemple simple d’un fichier d’écouteurs en veille prolongée pour spring boot (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;
    }
}
7
Sllouyssgort

J'ai eu un problème similaire avec une application Spring 4.1.1, Hibernate 4.3.11 - pas avec Spring Boot. 

La solution que j’ai trouvée (après avoir lu le code Hibernate EntityManagerFactoryBuilderImpl) était que, si vous transmettez une référence de bean au lieu d’un nom de classe à la propriété hibernate.ejb.interceptor de la définition du gestionnaire d’entités, Hibernate utilisera ce bean déjà instancié.

Donc, dans ma définition de entityManager en contexte d'application, j'avais quelque chose comme ceci:

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

L'auditInterceptor est géré par Spring. Par conséquent, le câblage automatique et les autres comportements associés à Spring sont disponibles.

3
BhathiyaW

Après une recherche de deux jours sur la manière d’intégrer Hibernate Interceptors à Spring Data JPA, j’ai trouvé une autre approche. Ma solution est un hybride entre la configuration Java et la configuration xml, mais this post a été très utile. Donc ma solution finale était:

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

}

Configuration Java de la source de données:

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

Gestionnaires d'entités et de transactions ajoutant l'intercepteur

<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" />

fichier de configuration de persistance

     <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>
2
kristianva

Bonjour,

Donnez cette lecture: https://github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d9fb723d0e334fe3e7d9be (utilisation: interface HibernatePropertiesCustomizer)

OU

Pour un intercepteur simple:

Afin de configurer cela dans votre application, vous devez simplement ajouter: spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor (dans application.properties). L'intercepteur lui-même devrait être @Component.

Tant que l'intercepteur n'utilise pas de haricot. Sinon, c'est un peu plus compliqué mais je serais plus qu'heureux de proposer la solution.

N'oubliez pas d'ajouter dans application-test.properties, un EmptyInterceptor pour ne pas utiliser le système de journalisation (ou ce que vous voulez utiliser pour) dans des tests (ce qui ne serait pas très utile).

J'espère que cela vous a été utile.

Remarque finale: mettez toujours à jour vos versions de Spring/Hibernate (utilisez la plus récente possible) et vous verrez que la plupart du code deviendra redondant, car les versions les plus récentes tentent de réduire autant que possible les configurations.

2
Rareș Flueraș

Parce que l'intercepteur ne s'enregistre pas comme un haricot de printemps, vous pouvez utiliser un util qui peut obtenir une instance de ApplicationContext, comme ceci:

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

Ensuite, vous pouvez appeler le service dans l'intercepteur, comme ceci:

public class SimpleInterceptor extends EmptyInterceptor {

   @Override
   public String onPrepareStatement(String sql) {
       MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
       myService.print();
    return super.onPrepareStatement(sql);
   }
 }
0
Null

J'ai rencontré le même problème et j'ai fini par créer une petite bibliothèque printanière pour gérer toute la configuration.

https://github.com/teastman/spring-data-hibernate-event

Si vous utilisez Spring Boot, ajoutez simplement la dépendance:

<dependency>
  <groupId>io.github.teastman</groupId>
  <artifactId>spring-data-hibernate-event</artifactId>
  <version>1.0.0</version>
</dependency>

Ajoutez ensuite l'annotation @HibernateEventListener à toute méthode dont le premier paramètre est l'entité que vous souhaitez écouter et le second paramètre est l'événement Hibernate que vous souhaitez écouter. J'ai également ajouté la fonction statique d'utilisation getPropertyIndex pour accéder plus facilement à la propriété spécifique que vous souhaitez vérifier, mais vous pouvez également simplement regarder l'événement Hibernate brut.

@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
  int index = getPropertyIndex(event, "name");
  if (event.getOldState()[index] != event.getState()[index]) {
    // The name changed.
  }
}
0
Tyler Eastman