纠正互联网上关于捕获异常事务可提交的言论

Posted 书唐瑞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了纠正互联网上关于捕获异常事务可提交的言论相关的知识,希望对你有一定的参考价值。


互联网上流传着这么一句'定律':如果事务中通过try...catch...捕获异常,事务可正常提交.

此篇文章,我们验证下它的严谨性

【场景一】

@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus() 
	try 
	  	update order set status = 6 where id=1;
  		int i = 3 / 0;
	 catch (Exception ignored) 


【场景二】

@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus() 
	try 
	  	update order set status = 6 where id=1;
  		service.calculate();	
	 catch (Exception ignored) 


public void calculate() 
	int i = 3 / 0;



【场景三】

@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus() 
	try 
	  	update order set status = 6 where id=1;
  		service.calculate();	
	 catch (Exception ignored) 


@Transactional(rollbackFor = Exception.class)
public void calculate() 
	int i = 3 / 0;

假设 updateOrderStatus 和 calculate 方法的事务均生效,不考虑事务不生效的情况.


那么以上3种场景,方法 updateOrderStatus 执行结束之后,updateOrderStatus 方法是否能正常提交事务呢?

对于场景一和场景二来说,updateOrderStatus 方法可以正常提交,读者朋友应该不会有异议


接下来主要分析场景三的情况.
@Transactional(rollbackFor = Exception.class)
public void updateOrderStatus() 
	try 
	  	update order set status = 6 where id=1;
  		service.calculate();	
	 catch (Exception ignored) 


@Transactional(rollbackFor = Exception.class)
public void calculate() 
	int i = 3 / 0;

假设 updateOrderStatus 方法是小赵写的方法, calculate 方法是项目中已有的功能方法,小赵只是调用了现有的 calculate 方法.
由于小赵的 updateOrderStatus 方法必须要保证,即便出现异常,也要提交事务,因此他使用了try … catch.


首先,在这种场景下,updateOrderStatus 和 calculate 方法都加了 @Transactional 注解,使用了默认的传播特性 Propagation.REQUIRED ,因此 只有一个事务. 即 updateOrderStatus 和 calculate 方法在同一个事务里.


calculate 方法抛的异常(java.lang.ArithmeticException: / by zero)虽然被外层的 updateOrderStatus 方法捕获了,但是calculate 方法抛的异常首先是被Spring捕获, 因为calculate 方法加了@Transactional 注解,Spring是第一个感知到calculate 方法抛的异常,这个时候Spring就会把当前事务标记成回滚状态.
// org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable 

	if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) 

		TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

		Object retVal;
		try 
			// 调用业务方法
			retVal = invocation.proceedWithInvocation();
		
		catch (Throwable ex) 
			// 内部会将事务标记成回滚
			completeTransactionAfterThrowing(txInfo, ex);
			// 异常继续向上抛出
			throw ex;
		
		finally 
			cleanupTransactionInfo(txInfo);
		
	


// completeTransactionAfterThrowing 方法会调用到 doSetRollbackOnly方法

// org.springframework.jdbc.datasource.DataSourceTransactionManager#doSetRollbackOnly
protected void doSetRollbackOnly(DefaultTransactionStatus status) 
    DataSourceTransactionObject txObject = (DataSourceTransactionObject)status.getTransaction();

    txObject.setRollbackOnly();


public void setRollbackOnly() 
    this.getConnectionHolder().setRollbackOnly();


public void setRollbackOnly() 
	this.rollbackOnly = true;

Spring将事务标记成了 rollbackOnly = true,即当前事务只能回滚.
即便外层的 updateOrderStatus 方法捕获了异常,一旦 updateOrderStatus 方法提交事务,就会提示如下错误




如果在异常发生的地方(即calculate 方法内部),和捕获异常的地方(即updateOrderStatus 方法内部),这中间如果有Spring的事务代码(比如使用了@Transactional ),那么Spring是第一个感知到异常,即便业务代码中加了try … catch…也无济于事,Spring依然不会允许提交事务. 如果这中间没有Spring的事务代码,即Spring没有感知到业务代码中的异常,而且业务代码中又使用了try … catch…,那么Spring就会允许提交事务.

当容器管理的 tx EJB 提交时,如何捕获和包装 JTA 抛出的异常?

【中文标题】当容器管理的 tx EJB 提交时,如何捕获和包装 JTA 抛出的异常?【英文标题】:How to catch and wrap exceptions thrown by JTA when a container-managed-tx EJB commits? 【发布时间】:2012-07-31 06:18:10 【问题描述】:

我正在努力解决管理非平凡数据模型的 EJB3 类的问题。当我的容器管理的事务方法提交时,我抛出了约束验证异常。我想防止它们被包裹在EJBException 中,而是抛出一个调用者可以处理的合理的应用程序异常。

要将其包装在合适的应用程序异常中,我必须能够捕获它。大多数时候,一个简单的 try/catch 就可以完成这项工作,因为验证异常是从我进行的 EntityManager 调用中引发的。

不幸的是,某些约束仅在提交时检查。例如,映射集合上的@Size(min=1) 违规只有在容器管理的事务提交时才会被捕获,一旦它在我的事务方法结束时离开我的控制。我无法捕获验证失败时引发的异常并将其包装,因此容器将其包装在javax.transaction.RollbackException 中并将that 包装在受诅咒的EJBException 中。调用者必须捕获所有 EJBExceptions 并深入原因链以尝试找出它是否是一个验证问题,这真的不好。

我正在处理容器管理的事务,所以我的 EJB 如下所示:

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


... 其中AppValidationException 是已检查异常或未检查异常,注释为@ApplicationException,因此它不会被EJB3 包装。

有时我可以使用EntityManager.flush() 触发早期约束违规并抓住它,但并非总是如此。即便如此,我真的也希望能够在提交时捕获由延迟约束检查引发的数据库级约束违规,并且这些只会在 JTA 时出现永远提交。

帮助?


已经尝试过:

Bean 管理的事务 将通过允许我在我控制的代码中触发提交来解决我的问题。不幸的是,它们不是一个选项,因为 bean 管理的事务不提供任何等效的 TransactionAttributeType.REQUIRES_NEW - 没有办法使用 BMT 暂停事务。 JTA 令人讨厌的疏忽之一。

见:

Why we need JTA 2.0 Bean-Managed Transaction Suspension in J2EE(不要这样做!)

...但请参阅注意事项和详细信息的答案。

javax.validation.ValidationException 是 JDK 异常;我无法修改它以添加 @ApplicationException 注释 以防止包装。我不能子类化它来添加注释;它是由 EclpiseLink 抛出的,而不是我的代码。我不确定将其标记为 @ApplicationException 是否会阻止 Arjuna(AS7 的 JTA impl)将其包装在 RollbackException 中。

我尝试像这样使用 EJB3 拦截器

@AroundInvoke
protected Object exceptionFilter(InvocationContext ctx) throws Exception 
    try 
        return ctx.proceed();
     catch (ValidationException ex) 
        throw new SomeAppException(ex);
    

...但似乎拦截器在内部 JTA(这是明智的,通常是可取的)触发,所以我想要捕获的异常还没有被抛出。

我想我想要的是能够定义一个在 JTA 完成它的事情之后应用的异常过滤器。有什么想法吗?


我正在使用 JBoss AS 7.1.1.Final 和 EclipseLink 2.4.0。 EclipseLink 根据these instructions 安装为 JBoss 模块,但这对于手头的问题并不重要。


更新:在对这个问题进行了更多思考之后,我意识到除了 JSR330 验证异常之外,我真的还需要能够从 DB 中捕获 SQLIntegrityConstraintViolationException 和 deadlock or serialization failure rollbacks with SQLSTATE 40P01 and 40001 respectively .这就是为什么试图确保提交永远不会抛出的方法不会很好地工作的原因。已检查的应用程序异常不能通过 JTA 提交抛出,因为 JTA 接口自然不会声明它们,但未检查的 @ApplicationException 注释异常应该可以。

似乎在任何我可以有效地捕获应用程序异常的地方,我也可以 - 尽管不那么漂亮 - 捕获 EJBException 并深入研究 JTA 异常和底层验证或 JDBC 异常,然后基于此做出决策。如果没有 JTA 中的异常过滤器功能,我可能不得不这样做。

【问题讨论】:

在我尝试回答之前澄清一下:ValidationException 是第三方例外,你说?不是来自javax.validation.* 包? 抱歉,措辞不佳。 “我没有在我的代码中定义的一个我无法控制也无法修改的异常”。 javax.validation.ValidationException. 最后一个问题:主动验证 (docs.oracle.com/javaee/6/api/javax/validation/Validator.html) 是不可能的? (我想它是;EclipseLink 使用代理的方式有一些问题,或者有一些东西阻止了它在 JPA 提供者提供的集合类上工作。) @LairdNelson Flushing 实体管理器会自动进行验证。它对抛出SQLIntegrityConstraintViolationException 的数据库级约束没有帮助,我也想处理这些约束,而且我仍然看到 ValidationExceptions 能够通过的奇怪情况,尽管进行了积极的刷新和手动验证。我还不确定为什么,但我希望能够过滤和包装它们。 您可以使用 XML 描述符来注释不受您控制(第 3 方)的代码(异常)。 【参考方案1】:

我在原始问题中所说的 REQUIRES_NEW 和 BMT 有一个警告。

参见 EJB 3.1 规范,13.6.1Bean-Managed Transaction Demarcation 部分,容器职责。上面写着:

容器必须管理对企业 bean 的客户端调用 具有 bean 管理的事务划分的实例如下。当一个 客户端通过企业 bean 的一个调用业务方法 在客户端视图中,容器会暂停任何可能发生的事务 与客户端请求相关联如果有交易 与实例相关联(如果有状态会话会发生这种情况 bean 实例在之前的某个业务中启动了事务 方法),容器将方法执行与此相关联 交易。如果有与 bean 关联的拦截器方法 实例,这些操作是在拦截器方法之前执行的 调用。

(斜体我的)。这很重要,因为这意味着 BMT EJB 不会继承具有关联容器管理 tx 的调用者的 JTA tx。任何当前的 tx 都暂停,因此如果 BMT EJB 创建一个 tx,它就是一个 事务,当它提交时,它只提交它的事务。

这意味着您可以使用 BMT EJB 方法开始并提交事务,就好像它实际上是 REQUIRES_NEW 并执行以下操作:

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


这将提交置于我的控制之下。在我不需要事务来跨越多个不同 EJB 的多个调用的情况下,这允许我在我的代码中处理所有错误,包括提交时的错误。

Java EE 6 tutorial page on bean managed transactions 没有提及这一点,也没有提及如何调用 BMT。

我链接到的博客中关于 BMT 无法模拟 REQUIRES_NEW 的说法是有效的,只是不清楚。如果您有一个 bean 管理的事务 EJB,则您不能暂停一个您已开始的事务 以开始另一个事务。调用单独的帮助程序 EJB 可能会暂停你的 tx 并给你相当于 REQUIRES_NEW 但我还没有测试过。


问题的另一半——在我需要容器管理事务的情况下,因为我必须跨多个不同的 EJB 和 EJB 方法完成工作——通过防御性编码来解决。

实体管理器的早期急切刷新允许我捕获我的 JSR330 验证规则可以找到的任何验证错误,因此我只需要确保它们是完整和全面的,这样我就不会收到任何检查约束或完整性违规错误提交时的数据库。我不能干净地处理它们,所以我需要真正防御性地避开它们。

防御性编码的一部分是:

在实体字段上大量使用javax.validation 注释,并在这还不够的情况下使用@AssertTrue 验证方法。 我很高兴看到集合的验证约束得到支持。例如,我有一个实体A,其集合为BA 必须至少有一个 B,所以我在B 的集合中添加了一个@Size(min=1) 约束,它在A 中定义。 自定义 JSR330 验证器。我为澳大利亚商业号码 (ABN) 等内容添加了几个自定义验证器,以确保我永远不会尝试向数据库发送无效验证器并触发数据库级验证错误。 使用EntityManager.flush() 提前刷新实体管理器。这会强制验证在您的代码控制下进行,而不是在 JTA 提交事务时进行。 我的 EJB 外观中的特定于实体的防御代码和逻辑,以确保不会出现 JSR330 验证无法检测到的情况并导致提交失败。 在可行的情况下,使用 REQUIRES_NEW 方法强制提前提交并允许我处理 EJB 中的故障、适当地重试等。这有时需要帮助 EJB 来解决自调用业务方法的问题。李>

我仍然无法像在 Swing 中直接使用 JDBC 那样优雅地处理和重试序列化失败或死锁,因此容器提供的所有这些“帮助”使我在某些方面倒退了几步。不过,它在其他地方节省了大量繁琐的代码和逻辑。

在发生这些错误的地方,我添加了一个 UI 框架级别的异常过滤器。它看到 EJBException 包装了 JTA RollbackException 包装了 PersistenceException 包装了 EclipseLink 特定的异常包装了 PSQLException,检查了 SQLState,并根据它做出决定。这是荒谬的迂回,但它有效

【讨论】:

【参考方案2】:

我没试过这个。但我猜这应该可行。

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

容器应该启动一个新事务并提交methodOfInterest,因此您应该能够在包装器方法中捕获异常。

Ps:答案是根据@LairdNelson 提供的优雅想法更新的...

【讨论】:

如果这样做就好了,但不幸的是,当 EJB 调用自己的方法时,AFAIK 不会通过拦截、JTA 等;这只是一个直接的调用。 TransactionAttributeType.REQUIRES_NEW 无效。我想到了一种类似的方法,将REQUIRES_NEW 方法拆分为一个单独的辅助EJB,但对于处理tx 提交不是立即的REQUIRES 方法来说,这(a)很丑陋并且(b)没有帮助。 然后用另一个 EJB 包装一个 EJB 怎么样。这样它应该拦截调用。 哦,抱歉,我错过了您评论的最后一部分。但是,您应该通过显式刷新或在调用 EJB 时处理验证异常。另一方面,我个人并不觉得这很丑陋,因为一个是持久层,很像 DAO,另一个是业务接口。 添加一个@Injected self实例字段,用于自调用。那么哈桑的解决方案应该会奏效。 @LairdNelson 这合法吗?我想知道这一点,但这篇文章 adam-bien.com/roller/abien/entry/how_to_self_invoke_ejb 及其 cmets 似乎另有说明。【参考方案3】:

eclipselink.exception-handler 属性设置为指向ExceptionHandler 的实现看起来很有希望,但没有成功。

ExceptionHandler 的 JavaDoc 是……糟糕……所以您需要查看 test implementation 和使用它的测试(1、2)。还有一些更有用的文档here。

似乎很难使用异常过滤器来处理一些特定情况,同时不影响其他所有情况。我想捕获PSQLException,检查 SQLSTATE 23514(CHECK 约束违规),为此抛出一个有用的异常,否则不会更改任何内容。这看起来不太实用。

最后,我放弃了这个想法,尽可能使用 bean 托管事务(现在我正确理解了它们的工作原理)和一种防御方法,以防止在使用 JTA 容器托管事务时出现不需要的异常。

【讨论】:

【参考方案4】:

javax.validation.ValidationException 是 JDK 异常;我不能 修改它以添加 @ApplicationException 注释以防止 包装

除了您的答案:您可以使用 XML 描述符将 3rd 方类注释为 ApplicationException。

【讨论】:

我不再使用这个(谢天谢地)但是为了帮助其他仍然在工作的人,你有没有机会添加一个文档链接? 使用部署描述符 (ejb-jar.xml)。 “应用程序异常部署描述符元素的回滚子元素可以显式指定以覆盖 ApplicationException 注释指定或默认的回滚值。” JSR 220 这里还有ejb-jar 3.1 Schema。大多数元素都是可选的,以支持部署配置扩充。搜索“应用程序异常”。不是最好的文档,但我发现在网上很难找到信息(在 Oracle 出现之前似乎要容易得多)。 非常感谢。我希望它能尽快帮助其他人。

以上是关于纠正互联网上关于捕获异常事务可提交的言论的主要内容,如果未能解决你的问题,请参考以下文章

Spring事务源码(1)-TransactionInterceptor

java基础子线程任务发生异常,主线程事务如何回滚?

Spring事务异常回滚

mysql存储过程事务和捕获异常信息

Java事务不回滚的原因总结

当容器管理的 tx EJB 提交时,如何捕获和包装 JTA 抛出的异常?