java-ee - transacciones - tipos de ejb
¿Cómo detectar y envolver las excepciones generadas por JTA cuando se confirma un EJB administrado por contenedor? (4)
Estoy luchando con un problema con una clase EJB3 que administra un modelo de datos no trivial. Se lanzan excepciones de validación de restricciones cuando se confirman mis métodos transaccionales administrados por contenedor. Quiero evitar que se EJBException
en EJBException
, en lugar de lanzar una excepción de aplicación EJBException
que los llamadores pueden manejar.
Para envolverlo en una excepción de aplicación adecuada, debo poder atraparla. La mayoría de las veces, un simple intento / captura hace el trabajo porque la excepción de validación se produce desde una llamada de EntityManager
que he realizado.
Desafortunadamente, algunas restricciones solo se verifican en el momento del compromiso. Por ejemplo, la violación de @Size(min=1)
en una colección asignada solo se @Size(min=1)
cuando se confirma la transacción administrada por el contenedor, una vez que deja mi control al final de mi método transaccional. No puedo capturar la excepción lanzada cuando falla la validación y envolverla, por lo que el contenedor la envuelve en una javax.transaction.RollbackException
y la envuelve en una EJBException
maldita. La persona que llama debe capturar todas las EJBException
e ir a bucear en la cadena de causas para intentar averiguar si se trata de un problema de validación, lo que realmente no es bueno.
Estoy trabajando con transacciones gestionadas por contenedor, por lo que mi EJB se ve así:
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER
class TheEJB {
@Inject private EntityManager em;
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public methodOfInterest() throws AppValidationException {
try {
// For demonstration''s sake create a situation that''ll cause validation to
// fail at commit-time here, like
someEntity.getCollectionWithMinSize1().removeAll();
em.merge(someEntity);
} catch (ValidationException ex) {
// Won''t catch violations of @Size on collections or other
// commit-time only validation exceptions
throw new AppValidationException(ex);
}
}
}
... donde AppValidationException
es una excepción marcada o una excepción no marcada anotada en @ApplicationException
para que no sea envuelta por EJB3.
A veces puedo desencadenar una infracción de restricción temprana con un EntityManager.flush()
y capturar eso, pero no siempre. Incluso entonces, realmente me gustaría poder atrapar las violaciones de restricciones en el nivel de la base de datos generadas por las comprobaciones de restricciones diferidas en el momento de la confirmación, y esas solo surgirán cuando JTA se comprometa.
¿Ayuda?
Ya probado:
Las transacciones administradas por Bean resolverían mi problema permitiéndome activar la confirmación dentro del código que controlo. Desafortunadamente, no son una opción porque las transacciones administradas por bean no ofrecen ningún equivalente de TransactionAttributeType.REQUIRES_NEW
; no hay forma de suspender una transacción utilizando BMT. Uno de los molestos descuidos de JTA.
Ver:
- Por qué necesitamos JTA 2.0
- Suspensión de transacciones gestionadas por Bean en J2EE (¡no haga esto!)
... pero vea las respuestas para advertencias y detalles.
javax.validation.ValidationException
es una excepción JDK; No puedo modificarlo para agregar una anotación @ApplicationException
para evitar el @ApplicationException
. No puedo subclasificarlo para agregar la anotación; Es lanzado por EclpiseLink, no mi código. No estoy seguro de que marcarlo en @ApplicationException
detendría a Arjuna (implícita de JTA de AS7) envolviéndolo en una RollbackException
todos modos.
Intenté usar un interceptor EJB3 como este:
@AroundInvoke
protected Object exceptionFilter(InvocationContext ctx) throws Exception {
try {
return ctx.proceed();
} catch (ValidationException ex) {
throw new SomeAppException(ex);
}
}
... pero parece que los interceptores se activan dentro de JTA (lo cual es sensato y generalmente deseable), por lo que aún no se ha lanzado la excepción que deseo capturar.
Supongo que lo que quiero es poder definir un filtro de excepción que se aplique después de que JTA haga su trabajo. ¿Algunas ideas?
Estoy trabajando con JBoss AS 7.1.1.Final y EclipseLink 2.4.0. EclipseLink se instala como un módulo JBoss según estas instrucciones , pero eso no importa mucho para el problema en cuestión.
ACTUALIZACIÓN: Después de pensar más en este tema, me he dado cuenta de que, además de las excepciones de validación JSR330, realmente también necesito poder interceptar SQLIntegrityConstraintViolationException del DB y interbloqueo o reversiones de fallos de serialización con SQLSTATE 40P01 y 40001 respectivamente . Es por eso que un enfoque que solo trata de asegurarse de que el lanzamiento nunca se lance no funcionará bien. Las excepciones de las aplicaciones comprobadas no se pueden lanzar a través de una confirmación JTA porque las interfaces JTA, naturalmente, no las declaran, pero las excepciones anotadas @ApplicationException
no deberían estar habilitadas.
Parece que en cualquier lugar en el que pueda capturar una excepción de aplicación también puedo, aunque con menos gracia, capturar una excepción EJB y ahondar en ella para la excepción JTA y la validación subyacente o la excepción JDBC, luego tomar decisiones basadas en eso. Sin una característica de filtro de excepción en JTA probablemente tendré que hacerlo.
javax.validation.ValidationException es una excepción JDK; No puedo modificarlo para agregar una anotación @ApplicationException para evitar el ajuste
Además de su respuesta: puede utilizar el descriptor XML para anotar clases de terceros como ApplicationException.
Establecer la propiedad eclipselink.exception-handler
para que apunte a una implementación de ExceptionHandler
parecía prometedor, pero no funcionó.
El JavaDoc para ExceptionHandler
es ... malo ... así que querrá ver la implementación de la prueba y las pruebas ( 1 , 2 ) que la usan. Hay documentación algo más útil here .
Parece difícil usar el filtro de excepción para manejar algunos casos específicos y no afecta a todo lo demás. Quería interceptar PSQLException
, verificar SQLSTATE 23514 (violación de restricción CHECK
), lanzar una excepción útil para eso y de lo contrario no cambiar nada. Eso no parece práctico.
Al final, dejé de lado la idea y me decidí por transacciones administradas por beans donde sea posible (ahora que entiendo bien cómo funcionan) y un enfoque defensivo para evitar excepciones no deseadas al usar transacciones administradas por contenedores JTA.
Hay una advertencia a lo que dije sobre REQUIRES_NEW
y BMT en la pregunta original.
Consulte la especificación EJB 3.1, sección 13.6.1 Demarcación de transacciones gestionadas por el Bean , en Responsabilidades del contenedor . Se lee:
El contenedor debe administrar las invocaciones de clientes a una instancia de enterprise bean con demarcación de transacciones administradas por bean de la siguiente manera. Cuando un cliente invoca un método de negocio a través de una de las vistas de cliente del enterprise bean, el contenedor suspende cualquier transacción que pueda estar asociada con la solicitud del cliente . Si hay una transacción asociada con la instancia (esto sucedería si una instancia de bean de sesión con estado inició la transacción en algún método comercial anterior), el contenedor asocia la ejecución del método con esta transacción . Si hay métodos de interceptor asociados con las instancias de bean, estas acciones se toman antes de invocar los métodos de interceptor.
(cursiva mía). Eso es importante, porque significa que un EJB de BMT no hereda el tx JTA de un llamador que tiene un tx gestionado asociado. Cualquier tx actual se suspende , por lo que si un EJB de BMT crea un tx es una nueva transacción, y cuando se compromete, solo se compromete con su transacción.
Eso significa que puedes usar un método BMT EJB que comienza y confirma una transacción como si fuera efectivamente REQUIRES_NEW
y hacer algo como esto:
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
class TheEJB {
@Inject private EntityManager em;
@Resource private UserTransaction tx;
// Note: Any current container managed tx gets suspended at the entry
// point to this method; it''s effectively `REQUIRES_NEW`.
//
public methodOfInterest() throws AppValidationException, SomeOtherAppException {
try {
tx.begin();
// For demonstration''s sake create a situation that''ll cause validation to
// fail at commit-time here, like
someEntity.getCollectionWithMinSize1().removeAll();
em.merge(someEntity);
tx.commit();
} catch (ValidationException ex) {
throw new AppValidationException(ex);
} catch (PersistenceException ex) {
// Go grubbing in the exception for useful nested exceptions
if (isConstraintViolation(ex)) {
throw new AppValidationException(ex);
} else {
throw new SomeOtherAppException(ex);
}
}
}
}
Esto pone el commit bajo mi control. Cuando no necesito una transacción para abarcar varias llamadas a través de múltiples EJB, esto me permite manejar todos los errores, incluidos los errores en el momento de la confirmación, dentro de mi código.
La página de tutorial de Java EE 6 sobre transacciones administradas por bean no menciona esto, ni nada más acerca de cómo se llaman los BMT.
La charla sobre BMT que no puede simular REQUIRES_NEW
en los blogs a los que me he vinculado es válida, pero no está clara. Si tiene un EJB transaccional administrado por bean, no puede suspender una transacción que comenzó para comenzar otra. Una llamada a un EJB auxiliar auxiliar puede suspender su tx y darle el equivalente a REQUIRES_NEW
pero aún no he realizado la prueba.
La otra mitad del problema, los casos en los que necesito transacciones administradas por contenedor porque tengo un trabajo que debe realizarse a través de varios EJB diferentes y los métodos de EJB, se resuelve mediante la codificación defensiva.
El primer arranque impaciente del administrador de la entidad me permite detectar cualquier error de validación que puedan encontrar mis reglas de validación JSR330, por lo que solo necesito asegurarme de que sean completos y completos para que nunca obtenga ninguna restricción de verificación o errores de violación de integridad del DB en comprometer el tiempo. No puedo manejarlos limpiamente, así que necesito evitarlos realmente a la defensiva.
Parte de esa codificación defensiva es:
- El uso
javax.validation
anotacionesjavax.validation
en los campos de la entidad, y el uso de los métodos de validación de@AssertTrue
donde eso no es suficiente. - Se admiten restricciones de validación en colecciones, que me encantó ver. Por ejemplo, tengo una entidad
A
con una colección deB
A
debe tener al menos unaB
, así que agregué una@Size(min=1)
a la colección deB
donde está definida enA
- Validadores JSR330 personalizados. He agregado varios validadores personalizados para cosas como Australian Business Numbers (ABN) para asegurarme de que nunca intente enviar uno inválido a la base de datos y desencadenar un error de validación de nivel de base de datos.
-
EntityManager.flush()
administrador de la entidad conEntityManager.flush()
. Esto obliga a que la validación tenga lugar cuando está bajo el control de su código, y no más tarde cuando JTA va a confirmar la transacción. - El código y la lógica defensivos específicos de la entidad en mi fachada EJB para garantizar que las situaciones que no pueden ser detectadas por la validación JSR330 no se presenten y hagan que la confirmación falle.
- Cuando sea práctico, usar los métodos
REQUIRES_NEW
para forzar la confirmación temprana y permitirme manejar las fallas dentro de mis EJB, reintentar adecuadamente, etc. Esto a veces requiere un EJB auxiliar para solucionar los problemas con las llamadas a los métodos comerciales.
Todavía no puedo manejar con agrado y reintentar los fallos de serialización o los interbloqueos como lo hacía cuando estaba usando JDBC directamente con Swing, por lo que toda esta "ayuda" del contenedor me ha dado algunos pasos hacia atrás en algunas áreas. Sin embargo, guarda una gran cantidad de código y lógica complicados en otros lugares.
Donde se producen esos errores, he agregado un filtro de excepción de nivel UI-framework. Ve la EJBException
envolviendo la JTA RollbackException
envolviendo la PersistenceException
envolviendo la excepción específica de EclipseLink envolviendo la PSQLException
, examina el SQLState y toma decisiones basadas en eso. Es una rotonda absurda, pero funciona
No he probado esto. Pero supongo que esto debería funcionar.
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
class TheEJB {
@Inject
private TheEJB self;
@Inject private EntityManager em;
public methodOfInterest() throws AppValidationException {
try {
self.methodOfInterestImpl();
} catch (ValidationException ex) {
throw new AppValidationException(ex);
}
}
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public methodOfInterestImpl() throws AppValidationException {
someEntity.getCollectionWithMinSize1().removeAll();
em.merge(someEntity);
}
}
Se espera que el contenedor inicie una nueva transacción y se comprometa dentro del methodOfInterest
, por lo tanto, debería poder capturar la excepción en el método de contenedor.
Ps: La respuesta se actualiza en base a la idea elegante proporcionada por @LairdNelson ...