Listener de eventos de Hibernate con servicios de Spring

Escrito por picodotdev el .
java planeta-codigo programacion
Enlace permanente Comentarios

En el anterior artículo explicaba como crear un listener que reciba eventos de Hibernate pero quizá necesitemos en el listener hacer uso de un servicio de Spring si el proceso de la acción necesita aprovecharse de la funcionalidad proporcionada en los servicios. En este artículo mostraré como crear un listener de Hibernate que use un servicio de Spring, es decir, un listener de Hibernate con la posibilidad de inyectar servicios de Spring.

Para hacer la integración de los listeners con Spring debemos sustituir el Interceptor por un servicio que haga lo mismo pero al inicio del contenedor de Spring con la anotación @PostConstruct. Para ello creamos una clase con el siguiente contenido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package io.github.picodotdev.plugintapestry.services.hibernate;

import javax.annotation.PostConstruct;

import org.hibernate.SessionFactory;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.internal.SessionFactoryImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class HibernateConfigurer {

	@Autowired
	private SessionFactory sessionFactory;

	@Autowired
	private ProductoEventAdapter productoEventAdapter;
	
	@PostConstruct
	public void registerListeners() {
		SessionFactoryImpl sfi = (SessionFactoryImpl) sessionFactory;
	    EventListenerRegistry elr = sfi.getServiceRegistry().getService(EventListenerRegistry.class);

        elr.setListeners(EventType.PRE_INSERT, productoEventAdapter);
        elr.setListeners(EventType.PRE_UPDATE, productoEventAdapter);
        elr.setListeners(EventType.PRE_DELETE, productoEventAdapter);
        elr.setListeners(EventType.POST_INSERT, productoEventAdapter);
        elr.setListeners(EventType.POST_UPDATE, productoEventAdapter);
        elr.setListeners(EventType.POST_DELETE, productoEventAdapter);
	}
}
HibernateConfigurer.java

Configurando Spring con anotaciones y código Java, como es recomendable en vez de xml, la configuración del ejemplo es la siguiente y un archivo xml casi testimonial de Spring. En esta configuraicón vemos el servicio ProductoEventAdapter que usaremos para recibir los eventos y el servicio DummyService que se inyectará en el anterior:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package io.github.picodotdev.plugintapestry.spring;

...

@Configuration
@ComponentScan({ "io.github.picodotdev.plugintapestry" })
@EnableTransactionManagement
public class AppConfiguration {

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName(Driver.class.getCanonicalName());
        ds.setUrl("jdbc:h2:./misc/database/app");
        ds.setUsername("sa");
        ds.setPassword("sa");
        return ds;
    }

    // Hibernate
    @Bean(name = "sessionFactory")
    public LocalSessionFactoryBean sessionFactoryBean(DataSource dataSource) {
        Map<String, Object> m = new HashMap<>();
        m.put("hibernate.dialect", H2Dialect.class.getCanonicalName());
        m.put("hibernate.hbm2ddl.auto", "validate");
        // Debug
        m.put("hibernate.generate_statistics", true);
        m.put("hibernate.show_sql", true);

        Properties properties = new Properties();
        properties.putAll(m);

        //
        LocalSessionFactoryBean sf = new LocalSessionFactoryBean();
        sf.setDataSource(dataSource);
        sf.setPackagesToScan("io.github.picodotdev.plugintapestry.entities");
        sf.setHibernateProperties(properties);
        return sf;
    }

    // jOOQ
    @Bean
    public ConnectionProvider connectionProvider(DataSource dataSource) {
        return new DataSourceConnectionProvider(dataSource);
    }

    @Bean
    public ExecuteListenerProvider executeListenerProvider() {
        return new ExecuteListenerProvider() {
            @Override
            public ExecuteListener provide() {
                return new JooqExecuteListener();
            }
        };
    }

    @Bean
    public org.jooq.Configuration config(ConnectionProvider connectionProvider, ExecuteListenerProvider executeListenerProvider) {
        DefaultConfiguration config = new DefaultConfiguration();
        config.set(connectionProvider);
        config.set(SQLDialect.H2);
        config.set(executeListenerProvider);
        return config;
    }

    @Bean
    public DSLContext dsl(org.jooq.Configuration config) {
        return DSL.using(config);
    }

    @Bean
    public ServletContextInitializer initializer() {
        return new ServletContextInitializer() {
            @Override
            public void onStartup(ServletContext servletContext) throws ServletException {
                servletContext.setInitParameter("tapestry.app-package", "io.github.picodotdev.plugintapestry");
                servletContext.setInitParameter("tapestry.use-external-spring-context", "true");
                servletContext.addFilter("filter", AppFilter.class).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR), false, "/*");
                servletContext.addFilter("app", TapestrySpringFilter.class).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR), false, "/*");
                servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
            }
        };
    }

    // Tomcat
    @Bean
    public ConfigurableServletWebServerFactory webServerFactory() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
        connector.setScheme("http");
        connector.setSecure(false);
        connector.setPort(8080);

        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addAdditionalTomcatConnectors(connector);
        factory.getSession().setTimeout(Duration.ofMinutes(10));
        factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/error404"));
        factory.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error500"));
        return factory;
    }

    // Servicios
    @Bean
    public ProductoEventAdapter productoEventAdapter() {
        return new ProductoEventAdapter();
    }
    
    @Bean
    public HibernateProductoDAO hibenateProductoDAO(SessionFactory sessionFactory) {
        return new DefaultHibernateProductoDAO(sessionFactory);
    }
    
    @Bean
    public JooqProductoDAO jooqProductoDAO(DSLContext context) {
        return new DefaultJooqProductoDAO(context);
    }

    @Bean
    public DummyService dummyService() {
        return new DummyService();
    }
}
AppConfiguration.java

En el listener haremos uso de un servicio de Spring que podemos inyectar usando la anotación @Autorwire tal y como hacemos normalmente usando el contenedor de depednecias de Spring. La implementación con respecto a usar un listener con solo Hibernate varía ligeramente para adaptarse a los cambios de usar un servicio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package io.github.picodotdev.plugintapestry.services.hibernate;

import org.hibernate.event.spi.PostDeleteEvent;
import org.hibernate.event.spi.PostDeleteEventListener;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostInsertEventListener;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.event.spi.PreDeleteEvent;
import org.hibernate.event.spi.PreDeleteEventListener;
import org.hibernate.event.spi.PreInsertEvent;
import org.hibernate.event.spi.PreInsertEventListener;
import org.hibernate.event.spi.PreUpdateEvent;
import org.hibernate.event.spi.PreUpdateEventListener;
import org.hibernate.persister.entity.EntityPersister;

public class HibernateEventAdapter implements PreInsertEventListener, PostInsertEventListener, PreUpdateEventListener, PostUpdateEventListener, PreDeleteEventListener,
		PostDeleteEventListener {

	private static final long serialVersionUID = 1L;

	@Override
	public boolean requiresPostCommitHanding(EntityPersister persister) {
		return false;
	}

	@Override
	public void onPostDelete(PostDeleteEvent event) {
	}

	@Override
	public boolean onPreDelete(PreDeleteEvent event) {
		return false;
	}

	@Override
	public void onPostUpdate(PostUpdateEvent event) {
	}

	@Override
	public boolean onPreUpdate(PreUpdateEvent event) {
		return false;
	}

	@Override
	public void onPostInsert(PostInsertEvent event) {
	}

	@Override
	public boolean onPreInsert(PreInsertEvent event) {
		return false;
	}
}
HibernateEventAdapter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package io.github.picodotdev.plugintapestry.services.hibernate;

import org.hibernate.event.spi.PostDeleteEvent;
import org.hibernate.event.spi.PostInsertEvent;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PreDeleteEvent;
import org.hibernate.event.spi.PreInsertEvent;
import org.hibernate.event.spi.PreUpdateEvent;
import org.springframework.beans.factory.annotation.Autowired;

import io.github.picodotdev.plugintapestry.services.spring.DummyService;

public class ProductoEventAdapter extends HibernateEventAdapter {

	private static final long serialVersionUID = 1L;

	@Autowired
	private DummyService dummy;
	
	public void setDummy(DummyService dummy) {
		this.dummy = dummy;		
	}
	
	@Override
	public void onPostDelete(PostDeleteEvent event) {
		dummy.process("postDelete", event.getEntity());
	}

	@Override
	public boolean onPreDelete(PreDeleteEvent event) {
		dummy.process("preDelete", event.getEntity());
		return false;
	}

	@Override
	public void onPostUpdate(PostUpdateEvent event) {
		dummy.process("postUpdate", event.getEntity());
	}

	@Override
	public boolean onPreUpdate(PreUpdateEvent event) {
		dummy.process("preUpdate", event.getEntity());
		return false;
	}

	@Override
	public void onPostInsert(PostInsertEvent event) {
		dummy.process("postInsert", event.getEntity());
	}

	@Override
	public boolean onPreInsert(PreInsertEvent event) {
		dummy.process("preInsert", event.getEntity());
		return false;
	}
}
ProductoEventAdapter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.plugintapestry.services.spring;

import io.github.picodotdev.plugintapestry.entities.hibernate.Producto;

public class DummyService {

	public void process(String action, Object entity) {
		if (entity instanceof Producto) {
		    Producto p = (Producto) entity;
			System.out.println(String.format("Action: %s, Id: %d", action, p.getId()));
		}
	}
}
DummyService.java

En este ejemplo el resultado que veríamos en la consola sería el siguiente con las trazas Action: preInsert, Id: null y Action: postInsert, Id: 1 antes y después de ejecutarse la sentencia SQL:

Usando los listeners de Hibernate con Spring no necesitamos el archivo que creábamos antes en /META-INF/services/org.hibernate.integrator.spi.Integrator. Esto es un ejemplo de prueba de concepto pero perfectamente podría ser aplicado a una necesidad real. En el ejemplo PlugIn Tapestry que hice para un libro sobre el framework de desarrollo web Apache Tapestry puede verse el código completo y funcional de esta implementación.

Comparte el artículo: