Class TransactionInterceptor

java.lang.Object
com.ultikits.ultitools.aop.TransactionInterceptor
All Implemented Interfaces:
MethodInterceptor

public class TransactionInterceptor extends Object implements MethodInterceptor
AOP interceptor that implements declarative transaction management.

This interceptor processes @Transactional annotations and manages transaction boundaries accordingly. Each propagation is handled as follows:

  • REQUIRED: begins a new transaction when none is active; otherwise joins the active one as a participant (below).
  • REQUIRES_NEW: suspends whatever is active, runs in a new, independent transaction, and resumes the suspended one afterwards.
  • SUPPORTS: joins an active transaction as a participant; without one, the method runs with no transaction.
  • MANDATORY: joins an active transaction as a participant; without one, it throws IllegalStateException and the method does not run.
  • NOT_SUPPORTED: suspends whatever is active, runs with no transaction, and resumes the suspended one afterwards.
  • NEVER: throws IllegalStateException when a transaction is active; otherwise runs with no transaction.

A joined REQUIRED, SUPPORTS or MANDATORY method runs in a participation scope: begin(), then commit() when it returns, or rollback() when it throws an exception its own rollbackFor/noRollbackFor rules accept (otherwise commit()); its exception is rethrown unchanged. This relies on the TransactionManager nesting contract both built-in managers implement: begin() on an active transaction joins it, rollback() at a nested depth marks the transaction rollback-only without ending it, and the outermost commit() of a marked transaction rolls back and throws UnexpectedRollbackException. So a joined method's failure that its caller catches still rolls the whole transaction back, as Spring does by default (AbstractPlatformTransactionManager's globalRollbackOnParticipationFailure = true; the participant's rollback-or-commit choice follows TransactionAspectSupport's completeTransactionAfterThrowing). A joined method's isolation, read-only and timeout settings are not applied; they belong to the transaction that began it.

For a new transaction, the commit after a normal return runs after, not inside, the body's failure handler (TransactionAspectSupport's invokeWithinTransaction shape), so the transaction is completed once: a failing commit reaches the caller as it is, and when the method's rules commit despite its exception and that commit fails, the body's exception is kept on the commit failure as suppressed.

Since:
6.2.0
Author:
wisdomme
  • Field Details

  • Constructor Details

    • TransactionInterceptor

      public TransactionInterceptor(TransactionManager transactionManager)
      Creates a new transaction interceptor.
      Parameters:
      transactionManager - the transaction manager to use
  • Method Details

    • invoke

      public Object invoke(MethodInvocation invocation) throws Throwable
      Description copied from interface: MethodInterceptor
      Intercept the given method invocation.

      Implementations should call MethodInvocation.proceed() to continue the interceptor chain or invoke the target method.

      Specified by:
      invoke in interface MethodInterceptor
      Parameters:
      invocation - the method invocation context
      Returns:
      the result of the method invocation
      Throws:
      Throwable - if the interceptor or target method throws an exception
    • executeWithTransaction

      private Object executeWithTransaction(MethodInvocation invocation, Transactional tx) throws Throwable
      Executes the method invocation within a transaction context.
      Throws:
      Throwable
    • executeInNewTransaction

      private Object executeInNewTransaction(MethodInvocation invocation, Transactional tx) throws Throwable
      Executes the invocation in a new transaction.

      Only the body runs inside the try whose handler completes the transaction after a failure; the commit after a normal return runs after that handler, so a failing commit (UnexpectedRollbackException included) reaches the caller as it is and is never followed by a second completion call. When the body's exception does not call for a rollback and the commit then fails, the commit failure is thrown with the body's exception attached as suppressed.

      Throws:
      Throwable
    • executeAsParticipant

      private Object executeAsParticipant(MethodInvocation invocation, Transactional tx) throws Throwable
      Executes the invocation as a participant in the transaction already active on this thread.

      begin() joins the active transaction (one more nesting level). When the body returns, commit() leaves that level. When the body throws an exception the method's own rules accept (shouldRollback(java.lang.Throwable, com.ultikits.ultitools.annotations.Transactional)), rollback() at the nested level marks the whole transaction rollback-only without ending it; any other exception leaves it with commit(). The body's exception is always rethrown unchanged, with a failure of that completion call attached as suppressed. The method's isolation, read-only and timeout settings are not applied: they belong to the transaction that began it.

      Throws:
      Throwable
    • applySettings

      private void applySettings(Transactional tx, String methodName)
      Applies the isolation, read-only and timeout settings of tx to the transaction this invocation has just begun.

      When applying a setting fails, the transaction this invocation began is rolled back once, so its connection is released and no active context is left on the thread for the next call to join; any failure of that rollback is attached to the settings failure with Throwable.addSuppressed(Throwable), and the settings failure itself is rethrown unchanged. The method body never runs. A REQUIRES_NEW caller still resumes the suspended outer transaction in its own finally. This mirrors Spring's DataSourceTransactionManager.doBegin, which applies these settings inside its try and releases the connection before rethrowing.

      Parameters:
      tx - the transaction attributes of the invoked method
      methodName - the qualified method name, for logging
    • shouldRollback

      private boolean shouldRollback(Throwable e, Transactional tx)
      Determines if the transaction should be rolled back for the given exception.

      Additive, depth-ordered combination of rollbackFor and noRollbackFor (D-06/D-07 - confirmed by the maintainer during 02-06's checkpoint, matching Spring's RuleBasedTransactionAttribute): each array is checked for its shallowest matching inheritance depth via shallowestMatchDepth(Throwable, Class[]). When both arrays match, the shallower depth wins; an exact-depth tie - including the same exception class listed in both arrays - favours rollback. When only one array matches, that one decides. When neither matches (including when both arrays are empty), this falls through to the unchanged RuntimeException/Error default - a non-empty rollbackFor with no match no longer short-circuits to "commit" the way it did before this fix.

    • shallowestMatchDepth

      private Integer shallowestMatchDepth(Throwable thrown, Class<? extends Throwable>[] rules)
      Returns the shallowest inheritance depth at which thrown's runtime class matches any rule in rules, or null if none match. Depth 0 is an exact class match; each step up thrown's superclass chain toward a matching rule adds 1.
    • inheritanceDepth

      private int inheritanceDepth(Class<?> thrownClass, Class<?> ruleClass)
      Walks thrownClass's superclass chain to find the depth at which it matches ruleClass. Callers only reach this after confirming ruleClass.isInstance(...) of an instance of thrownClass already holds, so the walk is guaranteed to terminate at ruleClass rather than at null.