Ist es möglich, von Spring verwaltete Hibernate-Interceptors ( http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html ) in Spring Boot zu integrieren?
Ich verwende Spring Data JPA und Spring Data REST und brauche einen Hibernate-Interceptor, um eine Aktualisierung eines bestimmten Feldes in einer Entität durchzuführen.
Bei Standard-JPA-Ereignissen ist es nicht möglich, die alten Werte abzurufen. Daher denke ich, dass ich den Interceptor für den Ruhezustand verwenden muss.
Es gibt keine besonders einfache Möglichkeit, einen Hibernate-Interceptor hinzuzufügen, der auch eine Spring Bean ist. Sie können jedoch problemlos einen Interceptor hinzufügen, wenn er vollständig von Hibernate geändert wurde. Dazu fügen Sie Ihrem application.properties
Folgendes hinzu:
spring.jpa.properties.hibernate.ejb.interceptor=my.package.MyInterceptorClassName
Wenn der Interceptor auch eine Bean sein soll, können Sie Ihre eigene LocalContainerEntityManagerFactoryBean
erstellen. Die EntityManagerFactoryBuilder
von Spring Boot 1.1.4 ist bei den generischen Eigenschaften etwas zu restriktiv. Sie müssen also in (Map)
umgewandelt werden.
@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;
}
};
}
Die verschiedenen Threads als Referenz nehmend, endete die folgende Lösung:
Ich benutze Spring-Boot 1.2.3.RELEASE (das ist die aktuelle Ga im Moment)
Mein Anwendungsfall war der in diesem Fehler (DATAREST-373) beschriebene.
Ich musste in der Lage sein, das Kennwort einer User
@Entity
bei create zu kodieren und eine spezielle Logik bei save zu haben. Das Erstellen war sehr einfach mit @HandleBeforeCreate
und der Überprüfung der @Entity
-ID auf 0L
-Gleichheit.
Zum Speichern habe ich einen Hibernate Interceptor implementiert, der einen EmptyInterceptor erweitert
@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;
}
}
Bei Verwendung von Spring Boot wird in der Dokumentation darauf hingewiesen
alle Eigenschaften in spring.jpa.properties. * werden als normale JPA-Eigenschaften (mit entferntem Präfix) durchlaufen, wenn die lokale EntityManagerFactory erstellt wird.
Wie bereits erwähnt, können wir unseren Interceptor mithilfe von spring.jpa.properties.hibernate.ejb.interceptor
in unserer Spring-Boot-Konfiguration definieren. Ich konnte den @Autowire PasswordEncoder
jedoch nicht zum Laufen bringen.
Also habe ich auf HibernateJpaAutoConfiguration zurückgegriffen und protected void customizeVendorProperties(Map<String, Object> vendorProperties)
überschrieben. Hier ist meine Konfiguration.
@Configuration
public class HibernateConfiguration extends HibernateJpaAutoConfiguration{
@Autowired
Interceptor userInterceptor;
@Override
protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
vendorProperties.put("hibernate.ejb.interceptor",userInterceptor);
}
}
Die Interceptor
automatisch zu aktivieren, anstatt Hibernate die Instanziierung zu erlauben, war der Schlüssel, um sie zum Laufen zu bringen.
Was mich jetzt stört, ist, dass die Logik in zwei Hälften geteilt ist, aber wenn DATAREST-373 einmal gelöst ist, wird dies hoffentlich nicht nötig sein.
Mein einfaches Dateibeispiel für Hibernate-Listener für 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;
}
}
Ich hatte ein ähnliches Problem mit einer Spring 4.1.1, Hibernate 4.3.11-Anwendung - nicht mit Spring Boot.
Lösung (nachdem ich den Hibernate EntityManagerFactoryBuilderImpl-Code gelesen hatte) war, dass, wenn Sie einen Bean-Verweis anstelle eines Klassennamens an die hibernate.ejb.interceptor
-Eigenschaft der Entitätsmanager-Definition übergeben, Hibernate das bereits instantiierte Bean verwendet.
In meiner EntityManager-Definition im Anwendungskontext hatte ich so etwas:
<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>
Der auditInterceptor wird von Spring verwaltet. Daher stehen ihm automatische Einstellungen und andere Spring-Natured-Verhalten zur Verfügung.
Ich habe einen anderen Ansatz gefunden, nachdem ich zwei Tage recherchiert hatte, wie Hibernate Interceptors in Spring Data JPA integriert werden. Meine Lösung ist ein Hybrid aus Java-Konfiguration und XML-Konfiguration, aber this post war sehr nützlich. Meine endgültige Lösung war also:
AuditLogInterceptor-Klasse:
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;
}
}
Datenquelle Java-Konfiguration:
@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;
}
Entity- und Transaction Manager fügen den Interceptor hinzu
<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" />
persistenz-Konfigurationsdatei
<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>
Hallo,
Lesen Sie dies wie folgt: https://github.com/spring-projects/spring-boot/commit/59d5ed58428d8cb6c6d6f9fb723d0e334fe3e7d9be (use: HibernatePropertiesCustomizer interface)
ODER
Für einfachen Interceptor:
Um dies in Ihrer Anwendung zu konfigurieren, müssen Sie einfach Folgendes hinzufügen: spring.jpa.properties.hibernate.ejb.interceptor = path.to.interceptor (in application.properties). Der Interceptor selbst sollte @Component sein.
Solange der Interceptor tatsächlich keine Beans verwendet. Ansonsten ist es etwas komplizierter, aber ich würde die Lösung gerne anbieten.
Vergessen Sie nicht, in application-test.properties einen EmptyInterceptor hinzuzufügen, um das Protokollierungssystem (oder was auch immer Sie es verwenden möchten) in Tests nicht zu verwenden (was nicht sehr hilfreich wäre).
Ich hoffe, das war für Sie von Nutzen.
Als letzten Hinweis: Aktualisieren Sie Ihre Spring-/Hibernate-Versionen (verwenden Sie möglichst die neueste), und Sie werden feststellen, dass der meiste Code überflüssig wird, da neuere Versionen versuchen, die Konfigurationen so weit wie möglich zu reduzieren.
Da sich der Interceptor nicht als Spring Bean registriert, können Sie ein util verwenden, das eine ApplicationContext
-Instanz erhält, wie folgt:
@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;
}
}
Dann können Sie den Dienst im Interceptor wie folgt aufrufen:
public class SimpleInterceptor extends EmptyInterceptor {
@Override
public String onPrepareStatement(String sql) {
MyService myService=SpringContextUtil.getApplicationContext().getBean(MyService.class);
myService.print();
return super.onPrepareStatement(sql);
}
}
@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);
}
}
hibernate.session_factory.interceptor
Sie können hibernate.ejb.interceptor
. Beide Eigenschaften funktionieren wahrscheinlich aufgrund einer Abwärtskompatibilitätsanforderung.Eine vorgeschlagene Antwort besteht darin, Ihren Abfangjäger im spring.jpa.properties.hibernate.ejb.interceptor
Eigenschaft in application.properties/yml. Diese Idee funktioniert möglicherweise nicht, wenn sich Ihr Abfangjäger in einer Bibliothek befindet, die von mehreren Anwendungen verwendet wird. Sie möchten, dass Ihr Interceptor durch Hinzufügen einer Abhängigkeit zu Ihrer Bibliothek aktiviert wird, ohne dass jede Anwendung ihre Anwendung ändern muss. Eigenschaften.
Ich bin auf das gleiche Problem gestoßen und habe am Ende eine kleine Frühlingsbibliothek erstellt, um alle Einstellungen zu erledigen.
https://github.com/teastman/spring-data-hibernate-event
Wenn Sie Spring Boot verwenden, fügen Sie einfach die Abhängigkeit hinzu:
<dependency>
<groupId>io.github.teastman</groupId>
<artifactId>spring-data-hibernate-event</artifactId>
<version>1.0.0</version>
</dependency>
Fügen Sie dann die Annotation @HibernateEventListener zu jeder Methode hinzu, bei der der erste Parameter die Entität ist, die Sie abhören möchten, und der zweite Parameter das Hibernate-Ereignis, auf das Sie abhören möchten. Ich habe auch die statische util-Funktion getPropertyIndex hinzugefügt, um den Zugriff auf die bestimmte Eigenschaft, die Sie überprüfen möchten, einfacher zu erhalten. Sie können jedoch auch nur das rohe Hibernate-Ereignis betrachten.
@HibernateEventListener
public void onUpdate(MyEntity entity, PreUpdateEvent event) {
int index = getPropertyIndex(event, "name");
if (event.getOldState()[index] != event.getState()[index]) {
// The name changed.
}
}